ltcai 11.2.0 → 11.4.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 +46 -53
- package/docs/CHANGELOG.md +61 -0
- package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
- package/docs/DEVELOPMENT.md +1 -1
- package/docs/MULTI_AGENT_RUNTIME.md +1 -1
- package/docs/ONBOARDING.md +1 -1
- package/docs/OPERATIONS.md +6 -2
- package/docs/PERMISSION_MODE.md +1 -1
- package/docs/TRUST_MODEL.md +1 -1
- package/docs/WHY_LATTICE.md +1 -1
- package/docs/kg-schema.md +2 -2
- package/docs/v11.3.0_PLAN.md +202 -0
- package/docs/v11.4.0_RUST_FOUNDATION_PLAN.md +176 -0
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/graph/_kg_common/__init__.py +287 -0
- package/lattice_brain/graph/_kg_common/extraction.py +516 -0
- package/lattice_brain/graph/_kg_common/relations.py +161 -0
- package/lattice_brain/graph/_kg_common/text.py +479 -0
- package/lattice_brain/graph/discovery_index/__init__.py +35 -0
- package/lattice_brain/graph/discovery_index/cleanup.py +182 -0
- package/lattice_brain/graph/discovery_index/extract.py +137 -0
- package/lattice_brain/graph/discovery_index/scan.py +411 -0
- package/lattice_brain/graph/discovery_index/upsert.py +495 -0
- package/lattice_brain/graph/projection/__init__.py +42 -0
- package/lattice_brain/graph/projection/curation.py +500 -0
- package/lattice_brain/graph/{projection.py → projection/v2_schema.py} +15 -477
- package/lattice_brain/graph/retrieval/__init__.py +54 -0
- package/lattice_brain/graph/retrieval/context.py +197 -0
- package/lattice_brain/graph/retrieval/graph_view.py +319 -0
- package/lattice_brain/graph/retrieval/hybrid.py +488 -0
- package/lattice_brain/graph/retrieval/maintenance.py +121 -0
- package/lattice_brain/graph/retrieval/signals.py +95 -0
- package/lattice_brain/graph/retrieval_vector/__init__.py +42 -0
- package/lattice_brain/graph/retrieval_vector/fingerprint.py +97 -0
- package/lattice_brain/graph/retrieval_vector/indexing.py +347 -0
- package/lattice_brain/graph/retrieval_vector/search.py +560 -0
- package/lattice_brain/graph/retrieval_vector/status.py +374 -0
- package/lattice_brain/ingestion/__init__.py +130 -0
- package/lattice_brain/ingestion/_contract.py +90 -0
- package/lattice_brain/ingestion/constants.py +127 -0
- package/lattice_brain/ingestion/folder_scan.py +57 -0
- package/lattice_brain/ingestion/folders.py +258 -0
- package/lattice_brain/ingestion/hashing.py +26 -0
- package/lattice_brain/ingestion/jobs_api.py +107 -0
- package/lattice_brain/ingestion/models.py +80 -0
- package/lattice_brain/ingestion/pipeline.py +486 -0
- package/lattice_brain/ingestion/quality.py +209 -0
- package/lattice_brain/ingestion/routing.py +295 -0
- package/lattice_brain/multimodal/__init__.py +164 -0
- package/lattice_brain/multimodal/audio.py +77 -0
- package/lattice_brain/multimodal/common.py +118 -0
- package/lattice_brain/multimodal/images.py +498 -0
- package/lattice_brain/multimodal/ports.py +169 -0
- package/lattice_brain/multimodal/video.py +410 -0
- package/lattice_brain/portability/__init__.py +90 -0
- package/lattice_brain/portability/_contract.py +42 -0
- package/lattice_brain/portability/backups.py +338 -0
- package/lattice_brain/portability/bundles.py +136 -0
- package/lattice_brain/portability/constants.py +93 -0
- package/lattice_brain/portability/fsops.py +138 -0
- package/lattice_brain/portability/service.py +41 -0
- package/lattice_brain/{portability.py → portability/sharing.py} +44 -677
- package/lattice_brain/runtime/__init__.py +1 -1
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/chronicle.py +63 -0
- package/latticeai/core/agent/__init__.py +93 -0
- package/latticeai/core/agent/_contract.py +79 -0
- package/latticeai/core/agent/context.py +57 -0
- package/latticeai/core/agent/deps.py +125 -0
- package/latticeai/core/agent/execution.py +622 -0
- package/latticeai/core/agent/planning.py +145 -0
- package/latticeai/core/agent/recovery.py +157 -0
- package/latticeai/core/agent/runtime.py +210 -0
- package/latticeai/core/agent/verification.py +231 -0
- package/latticeai/core/embedding_providers/__init__.py +151 -0
- package/latticeai/core/embedding_providers/base.py +199 -0
- package/latticeai/core/embedding_providers/captions.py +162 -0
- package/latticeai/core/embedding_providers/profiles.py +126 -0
- package/latticeai/core/embedding_providers/text.py +350 -0
- package/latticeai/core/embedding_providers/vision.py +352 -0
- package/latticeai/core/file_generation/__init__.py +115 -0
- package/latticeai/core/file_generation/bundles.py +76 -0
- package/latticeai/core/file_generation/extraction.py +154 -0
- package/latticeai/core/file_generation/inference.py +235 -0
- package/latticeai/core/file_generation/orchestration.py +152 -0
- package/latticeai/core/file_generation/prompting.py +117 -0
- package/latticeai/core/file_generation/repair.py +114 -0
- package/latticeai/core/file_generation/sanitize.py +61 -0
- package/latticeai/core/file_generation/validation.py +201 -0
- package/latticeai/core/legacy_compatibility.py +1 -1
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/messages.py +9 -0
- package/latticeai/core/workspace_os_constants.py +1 -1
- package/latticeai/integrations/telegram_bot/__init__.py +123 -0
- package/latticeai/integrations/telegram_bot/__main__.py +17 -0
- package/latticeai/integrations/telegram_bot/config.py +86 -0
- package/latticeai/integrations/telegram_bot/dispatch.py +311 -0
- package/latticeai/integrations/telegram_bot/flows.py +478 -0
- package/latticeai/integrations/telegram_bot/helpers.py +322 -0
- package/latticeai/integrations/telegram_bot/screens.py +394 -0
- package/latticeai/models/router/__init__.py +88 -0
- package/latticeai/models/router/_contract.py +66 -0
- package/latticeai/models/router/branding.py +56 -0
- package/latticeai/models/router/catalog.py +69 -0
- package/latticeai/models/router/documents.py +199 -0
- package/latticeai/models/router/errors.py +37 -0
- package/latticeai/models/router/generation.py +258 -0
- package/latticeai/models/router/loading.py +291 -0
- package/latticeai/models/router/local_models.py +85 -0
- package/latticeai/models/router/registry.py +147 -0
- package/latticeai/runtime/build_phases/__init__.py +82 -0
- package/latticeai/runtime/build_phases/features.py +407 -0
- package/latticeai/runtime/build_phases/foundation.py +555 -0
- package/latticeai/runtime/build_phases/web.py +492 -0
- package/latticeai/runtime/runtime_context.py +1 -0
- package/latticeai/services/architecture_readiness.py +48 -19
- package/latticeai/services/brain_intelligence/__init__.py +58 -0
- package/latticeai/services/brain_intelligence/_contract.py +71 -0
- package/latticeai/services/brain_intelligence/consistency.py +193 -0
- package/latticeai/services/brain_intelligence/constants.py +47 -0
- package/latticeai/services/brain_intelligence/digest.py +258 -0
- package/latticeai/services/brain_intelligence/health.py +331 -0
- package/latticeai/services/brain_intelligence/proposals.py +264 -0
- package/latticeai/services/brain_intelligence/sampling.py +84 -0
- package/latticeai/services/brain_intelligence/service.py +48 -0
- package/latticeai/services/chronicle.py +557 -0
- package/latticeai/services/memory_service/__init__.py +52 -0
- package/latticeai/services/memory_service/_contract.py +100 -0
- package/latticeai/services/memory_service/brief.py +431 -0
- package/latticeai/services/memory_service/constants.py +57 -0
- package/latticeai/services/memory_service/maintenance.py +138 -0
- package/latticeai/services/memory_service/manager.py +186 -0
- package/latticeai/services/memory_service/proof.py +136 -0
- package/latticeai/services/memory_service/recall.py +225 -0
- package/latticeai/services/memory_service/service.py +48 -0
- package/latticeai/services/memory_service/stores.py +110 -0
- package/latticeai/services/model_runtime/__init__.py +322 -0
- package/latticeai/services/model_runtime/cloud.py +87 -0
- package/latticeai/services/model_runtime/download.py +282 -0
- package/latticeai/services/model_runtime/engines.py +341 -0
- package/latticeai/services/model_runtime/loading.py +178 -0
- package/latticeai/services/model_runtime/service.py +129 -0
- package/latticeai/services/model_runtime/state.py +131 -0
- package/latticeai/services/model_runtime/status.py +255 -0
- package/latticeai/services/product_readiness.py +15 -7
- package/latticeai/setup/wizard/__init__.py +126 -0
- package/latticeai/setup/wizard/catalog.py +172 -0
- package/latticeai/setup/wizard/detect.py +323 -0
- package/latticeai/setup/wizard/install.py +348 -0
- package/latticeai/setup/wizard/paths.py +168 -0
- package/latticeai/setup/wizard/plans.py +74 -0
- package/latticeai/setup/wizard/recommend.py +320 -0
- package/package.json +6 -2
- package/scripts/bump_version.py +14 -0
- package/scripts/capture_release_evidence.mjs +33 -21
- package/scripts/check_current_release_docs.mjs +1 -1
- package/scripts/check_i18n_namespace_coverage.mjs +41 -4
- package/scripts/check_max_file_lines.mjs +102 -0
- package/scripts/check_release_evidence_bound.mjs +30 -15
- package/scripts/check_screenshot_pixel_delta.py +34 -4
- package/scripts/check_server_i18n.mjs +1 -0
- package/scripts/generate_rust_parity_fixtures.py +562 -0
- package/scripts/lib/mock_server_fingerprint.mjs +94 -0
- package/scripts/release_screen_claims.json +31 -2
- package/src-tauri/Cargo.lock +361 -3
- package/src-tauri/Cargo.toml +6 -1
- package/src-tauri/src/backend.rs +349 -0
- package/src-tauri/src/folder.rs +33 -0
- package/src-tauri/src/main.rs +97 -399
- package/src-tauri/tauri.conf.json +1 -1
- package/static/app/asset-manifest.json +41 -37
- package/static/app/assets/Act-yYpYnn0v.js +1 -0
- package/static/app/assets/AdminConsole-DL3Cr5pL.js +1 -0
- package/static/app/assets/{Brain-tuhI4sOC.js → Brain-C1HBN0Wf.js} +2 -2
- package/static/app/assets/BrainHome-DoXRhUUC.js +2 -0
- package/static/app/assets/BrainSignals-6yR6ir5t.js +1 -0
- package/static/app/assets/Capture-CFIRsFNE.js +1 -0
- package/static/app/assets/Chronicle-BZbEgiwN.js +1 -0
- package/static/app/assets/CommandPalette-D2pMxC2I.js +1 -0
- package/static/app/assets/Library-DwO3yZST.js +1 -0
- package/static/app/assets/{LivingBrain-DBwhto14.js → LivingBrain-Jn1GK0-S.js} +1 -1
- package/static/app/assets/ProductFlow-B-w1R4Oo.js +1 -0
- package/static/app/assets/ReviewCard-6B27X8Vg.js +3 -0
- package/static/app/assets/System-DW8F-2xL.js +1 -0
- package/static/app/assets/arrow-left-DXvKg9U6.js +1 -0
- package/static/app/assets/{bot-Cia42c2h.js → bot-IM_E_Y12.js} +1 -1
- package/static/app/assets/brain-Ci1CkWjM.js +1 -0
- package/static/app/assets/{button-2j2Ijzgq.js → button-COwyqfHM.js} +1 -1
- package/static/app/assets/circle-check-DfInj-qD.js +1 -0
- package/static/app/assets/{circle-pause-BEFeWpVW.js → circle-pause-DEM4A1Y5.js} +1 -1
- package/static/app/assets/{circle-play-ujXMcHxl.js → circle-play-C9djDuLd.js} +1 -1
- package/static/app/assets/{cpu-k4awryFq.js → cpu-DFdo1gw-.js} +1 -1
- package/static/app/assets/{download-DFbLJ_ig.js → download-SnJL6oqk.js} +1 -1
- package/static/app/assets/{folder-open-7y_b6xkM.js → folder-open-CqZeDkjE.js} +1 -1
- package/static/app/assets/{hard-drive-Bidh02Kr.js → hard-drive-j1jJXYYf.js} +1 -1
- package/static/app/assets/{index-DwDl9-8Y.css → index-BLPb5lmE.css} +1 -1
- package/static/app/assets/index-_u5iUHDr.js +10 -0
- package/static/app/assets/input-B0lPdRQZ.js +1 -0
- package/static/app/assets/link-2-CoFbooHS.js +1 -0
- package/static/app/assets/{permissionCopy-Bpb83Hx9.js → permissionCopy-BsyLxtao.js} +1 -1
- package/static/app/assets/primitives-DEbN-d6p.js +1 -0
- package/static/app/assets/search-BybIWPNd.js +1 -0
- package/static/app/assets/{share-2-BH1M-WNi.js → share-2-CVtZ_ewX.js} +1 -1
- package/static/app/assets/{shield-alert-BlKdBXcG.js → shield-alert-CBi2GNWM.js} +1 -1
- package/static/app/assets/{textarea-CCWbUfFB.js → textarea-DNMpB5ih.js} +1 -1
- package/static/app/assets/{useFocusTrap-YdHQ7pJ1.js → useFocusTrap-C83t3GXF.js} +1 -1
- package/static/app/assets/useMutation-DtbJDoyz.js +1 -0
- package/static/app/assets/{useQuery-CXQiwbVT.js → useQuery-Dcp1OChy.js} +1 -1
- package/static/app/assets/utils-BlZr7Pd4.js +4 -0
- package/static/app/assets/workspace-jJY4RuAV.js +1 -0
- package/static/app/index.html +4 -4
- package/static/sw.js +1 -1
- package/lattice_brain/graph/_kg_common.py +0 -1331
- package/lattice_brain/graph/discovery_index.py +0 -1141
- package/lattice_brain/graph/retrieval.py +0 -1120
- package/lattice_brain/graph/retrieval_vector.py +0 -1293
- package/lattice_brain/ingestion.py +0 -1525
- package/lattice_brain/multimodal.py +0 -1258
- package/latticeai/core/agent.py +0 -1465
- package/latticeai/core/embedding_providers.py +0 -1196
- package/latticeai/core/file_generation.py +0 -1047
- package/latticeai/integrations/telegram_bot.py +0 -1390
- package/latticeai/models/router.py +0 -1007
- package/latticeai/runtime/build_phases.py +0 -1450
- package/latticeai/services/brain_intelligence.py +0 -1083
- package/latticeai/services/memory_service.py +0 -1177
- package/latticeai/services/model_runtime.py +0 -1281
- package/latticeai/setup/wizard.py +0 -1310
- package/static/app/assets/Act-AWf0SAKp.js +0 -1
- package/static/app/assets/AdminConsole-D0u8Tiyj.js +0 -1
- package/static/app/assets/BrainHome-Ts7G_Ila.js +0 -2
- package/static/app/assets/BrainSignals-jMYgQ2Ar.js +0 -1
- package/static/app/assets/Capture-CqOSzyPr.js +0 -1
- package/static/app/assets/CommandPalette-DC0Bzh-I.js +0 -1
- package/static/app/assets/Library-CX-bbhmK.js +0 -1
- package/static/app/assets/ProductFlow-BHA2cfKI.js +0 -1
- package/static/app/assets/ReviewCard-BUhCKRNM.js +0 -3
- package/static/app/assets/System-Bu2t5hn1.js +0 -1
- package/static/app/assets/arrow-left-Dzwa5zRb.js +0 -1
- package/static/app/assets/brain-DJMoqrwx.js +0 -1
- package/static/app/assets/index-BpYkzcVm.js +0 -10
- package/static/app/assets/input-DSlJJxRs.js +0 -1
- package/static/app/assets/primitives-BCx6TvfG.js +0 -1
- package/static/app/assets/search-Cgy8cCFJ.js +0 -1
- package/static/app/assets/utils-zqPZJxdx.js +0 -4
- package/static/app/assets/workspace-DXTihhfU.js +0 -1
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""The seam the Brain Intelligence mixins share.
|
|
2
|
+
|
|
3
|
+
``BrainIntelligenceService`` is assembled from the proposal, sampling, health,
|
|
4
|
+
digest and consistency mixins. Each reads constructor state it does not own,
|
|
5
|
+
and several call methods another mixin implements — ``_graph_sample`` feeds
|
|
6
|
+
almost everything, ``_proactive`` gates the graph-layer surfaces, and the
|
|
7
|
+
garden view composes the contradiction scan. That contract existed as an
|
|
8
|
+
unwritten convention inside one class; this module writes it down.
|
|
9
|
+
|
|
10
|
+
Typing-only, exactly like :mod:`lattice_brain.portability._contract`: every
|
|
11
|
+
body below is a bare ``raise NotImplementedError`` that the composed class
|
|
12
|
+
always overrides, so the MRO and every method resolution stay what the
|
|
13
|
+
single-file class had. Adding a cross-mixin call without declaring it here is a
|
|
14
|
+
type error.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from typing import Any, Dict, List, Optional
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class BrainIntelligenceCore:
|
|
23
|
+
"""What any Brain Intelligence mixin may assume about ``self``.
|
|
24
|
+
|
|
25
|
+
Members are declared, not implemented: the implementation lives in
|
|
26
|
+
``service.py`` (constructor state) or in whichever mixin owns the method.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
# ── State owned by BrainIntelligenceService.__init__ ─────────────────────
|
|
30
|
+
_kg: Any
|
|
31
|
+
_memory: Any
|
|
32
|
+
_enable_graph: bool
|
|
33
|
+
_memory_quality: Any
|
|
34
|
+
_edge_quality: Any
|
|
35
|
+
_proactive_brain: Any
|
|
36
|
+
_review_queue_service: Any
|
|
37
|
+
_synthesizer: Any
|
|
38
|
+
|
|
39
|
+
# ── proposals.py: the review-queue seam every write door goes through ────
|
|
40
|
+
def _review_queue(self) -> Any:
|
|
41
|
+
raise NotImplementedError
|
|
42
|
+
|
|
43
|
+
def _synthesis(self) -> Any:
|
|
44
|
+
raise NotImplementedError
|
|
45
|
+
|
|
46
|
+
@staticmethod
|
|
47
|
+
def _no_queue(detail: str) -> Dict[str, Any]:
|
|
48
|
+
raise NotImplementedError
|
|
49
|
+
|
|
50
|
+
def importance_report(
|
|
51
|
+
self, *, user_email: Optional[str] = None, workspace_id: Optional[str] = None
|
|
52
|
+
) -> Dict[str, Any]:
|
|
53
|
+
raise NotImplementedError
|
|
54
|
+
|
|
55
|
+
# ── sampling.py: the graph slice and the memory tier ─────────────────────
|
|
56
|
+
def _proactive(self) -> Any:
|
|
57
|
+
raise NotImplementedError
|
|
58
|
+
|
|
59
|
+
def _graph_sample(self, *, workspace_id: Optional[str]) -> Dict[str, Any]:
|
|
60
|
+
raise NotImplementedError
|
|
61
|
+
|
|
62
|
+
def _workspace_memories(
|
|
63
|
+
self, *, user_email: Optional[str], workspace_id: Optional[str]
|
|
64
|
+
) -> List[Dict[str, Any]]:
|
|
65
|
+
raise NotImplementedError
|
|
66
|
+
|
|
67
|
+
# ── consistency.py: what the garden view composes ────────────────────────
|
|
68
|
+
def contradictions(
|
|
69
|
+
self, *, user_email: Optional[str] = None, workspace_id: Optional[str] = None
|
|
70
|
+
) -> Dict[str, Any]:
|
|
71
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""What the Brain disagrees with itself about, and what it says twice.
|
|
2
|
+
|
|
3
|
+
Two scans that wire the previously dormant quality layer
|
|
4
|
+
(:mod:`lattice_brain.quality`) to real stores:
|
|
5
|
+
|
|
6
|
+
* :meth:`contradictions` — four independent signals in one list: negation and
|
|
7
|
+
preference conflicts between workspace memories, temporal contradictions,
|
|
8
|
+
explicit ``CONTRADICTS`` edges already recorded in the graph, and the graph
|
|
9
|
+
layer's own node-content detector. Each item carries the signal that found
|
|
10
|
+
it, so a reviewer can weigh them differently.
|
|
11
|
+
* :meth:`consolidate` — duplicate memories and duplicate edges. Dry-run by
|
|
12
|
+
default, consent-first like every Brain automation. ``apply=True`` prunes
|
|
13
|
+
only exact duplicate *workspace memories*, through the audited MemoryService
|
|
14
|
+
path; graph content is never mutated here — the node merge plan is surfaced
|
|
15
|
+
for a governed apply path to adopt.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from typing import Any, Dict, List, Optional
|
|
21
|
+
|
|
22
|
+
from latticeai.core.timeutil import now_iso as _now
|
|
23
|
+
|
|
24
|
+
from ._contract import BrainIntelligenceCore as _Core
|
|
25
|
+
from .constants import LOGGER
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class BrainConsistencyMixin(_Core):
|
|
29
|
+
"""Contradiction and consolidation scans. Mixed into the service."""
|
|
30
|
+
|
|
31
|
+
# ── contradictions ───────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
def contradictions(
|
|
34
|
+
self, *, user_email: Optional[str] = None, workspace_id: Optional[str] = None
|
|
35
|
+
) -> Dict[str, Any]:
|
|
36
|
+
memories = self._workspace_memories(user_email=user_email, workspace_id=workspace_id)
|
|
37
|
+
memory_rows = [
|
|
38
|
+
{
|
|
39
|
+
"id": str(m.get("id") or f"mem-{i}"),
|
|
40
|
+
"content": str(m.get("content") or ""),
|
|
41
|
+
"score": 0.6,
|
|
42
|
+
"source": "workspace",
|
|
43
|
+
"timestamp": m.get("created_at") or m.get("timestamp") or 0,
|
|
44
|
+
}
|
|
45
|
+
for i, m in enumerate(memories)
|
|
46
|
+
if str(m.get("content") or "").strip()
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
conflicts: List[Dict[str, Any]] = []
|
|
50
|
+
candidates = self._memory_quality.extract_candidates(memory_rows)
|
|
51
|
+
for candidate in self._memory_quality.detect_conflicts(candidates):
|
|
52
|
+
pair_conflicts = [c for c in candidate.conflicts if c.startswith("conflict:contradicts:")]
|
|
53
|
+
for marker in pair_conflicts:
|
|
54
|
+
other_id = marker.rsplit(":", 1)[-1]
|
|
55
|
+
if any(
|
|
56
|
+
c["kind"] == "memory_pair"
|
|
57
|
+
and {c["left_id"], c["right_id"]} == {candidate.id, other_id}
|
|
58
|
+
for c in conflicts
|
|
59
|
+
):
|
|
60
|
+
continue
|
|
61
|
+
other: Dict[str, Any] | None = next(
|
|
62
|
+
(r for r in memory_rows if r["id"] == other_id), None
|
|
63
|
+
)
|
|
64
|
+
conflicts.append({
|
|
65
|
+
"kind": "memory_pair",
|
|
66
|
+
"left_id": candidate.id,
|
|
67
|
+
"left_content": candidate.content[:200],
|
|
68
|
+
"right_id": other_id,
|
|
69
|
+
"right_content": (other or {}).get("content", "")[:200],
|
|
70
|
+
"signal": "preference_negation",
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
temporal = self._memory_quality.detect_temporal_contradictions(memory_rows)
|
|
74
|
+
temporal_items = [
|
|
75
|
+
{
|
|
76
|
+
"kind": "temporal",
|
|
77
|
+
"id": item.get("id"),
|
|
78
|
+
"content": str(item.get("content") or "")[:200],
|
|
79
|
+
"signal": item.get("proactive_flag"),
|
|
80
|
+
}
|
|
81
|
+
for item in temporal
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
edge_items: List[Dict[str, Any]] = []
|
|
85
|
+
sample = self._graph_sample(workspace_id=workspace_id)
|
|
86
|
+
for edge in sample["edges"]:
|
|
87
|
+
if "CONTRADICT" in str(edge.get("type") or "").upper():
|
|
88
|
+
edge_items.append({
|
|
89
|
+
"kind": "graph_edge",
|
|
90
|
+
"id": edge.get("id"),
|
|
91
|
+
"source": edge.get("source") or edge.get("from_node"),
|
|
92
|
+
"target": edge.get("target") or edge.get("to_node"),
|
|
93
|
+
"signal": "contradicts_edge",
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
# v9.6.x additive: graph-layer node-content contradictions (proactive
|
|
97
|
+
# detector over node title/summary), on top of the memory + edge scans.
|
|
98
|
+
graph_pair_items: List[Dict[str, Any]] = []
|
|
99
|
+
proactive = self._proactive()
|
|
100
|
+
if proactive is not None:
|
|
101
|
+
try:
|
|
102
|
+
graph_result = proactive.detect_contradictions(workspace_id=workspace_id)
|
|
103
|
+
graph_pair_items = [
|
|
104
|
+
{"kind": "graph_node_pair", **pair}
|
|
105
|
+
for pair in graph_result.get("node_pairs") or []
|
|
106
|
+
]
|
|
107
|
+
except Exception:
|
|
108
|
+
LOGGER.exception("graph contradiction scan failed")
|
|
109
|
+
|
|
110
|
+
items = conflicts + temporal_items + edge_items + graph_pair_items
|
|
111
|
+
return {
|
|
112
|
+
"items": items,
|
|
113
|
+
"count": len(items),
|
|
114
|
+
"sources": {
|
|
115
|
+
"memory_pairs": len(conflicts),
|
|
116
|
+
"temporal": len(temporal_items),
|
|
117
|
+
"graph_edges": len(edge_items),
|
|
118
|
+
"graph_node_pairs": len(graph_pair_items),
|
|
119
|
+
},
|
|
120
|
+
"memories_scanned": len(memory_rows),
|
|
121
|
+
"generated_at": _now(),
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
# ── consolidation ────────────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
def consolidate(
|
|
127
|
+
self,
|
|
128
|
+
*,
|
|
129
|
+
apply: bool = False,
|
|
130
|
+
user_email: Optional[str] = None,
|
|
131
|
+
workspace_id: Optional[str] = None,
|
|
132
|
+
) -> Dict[str, Any]:
|
|
133
|
+
memories = self._workspace_memories(user_email=user_email, workspace_id=workspace_id)
|
|
134
|
+
memory_rows = [
|
|
135
|
+
{
|
|
136
|
+
"id": str(m.get("id") or f"mem-{i}"),
|
|
137
|
+
"content": str(m.get("content") or ""),
|
|
138
|
+
"score": 0.6,
|
|
139
|
+
"source": "workspace",
|
|
140
|
+
}
|
|
141
|
+
for i, m in enumerate(memories)
|
|
142
|
+
if str(m.get("content") or "").strip()
|
|
143
|
+
]
|
|
144
|
+
candidates = self._memory_quality.extract_candidates(memory_rows)
|
|
145
|
+
kept = self._memory_quality.dedupe(candidates)
|
|
146
|
+
kept_ids = {c.id for c in kept}
|
|
147
|
+
duplicate_memory_ids = [c.id for c in candidates if c.id not in kept_ids]
|
|
148
|
+
|
|
149
|
+
sample = self._graph_sample(workspace_id=workspace_id)
|
|
150
|
+
duplicate_edge_ids = [
|
|
151
|
+
edge_id
|
|
152
|
+
for edge_id in self._edge_quality.detect_duplicate_edges(sample["edges"])
|
|
153
|
+
if edge_id
|
|
154
|
+
]
|
|
155
|
+
|
|
156
|
+
pruned = 0
|
|
157
|
+
if apply and duplicate_memory_ids and self._memory is not None:
|
|
158
|
+
try:
|
|
159
|
+
result = self._memory.prune(
|
|
160
|
+
ids=duplicate_memory_ids,
|
|
161
|
+
user_email=user_email,
|
|
162
|
+
workspace_id=workspace_id,
|
|
163
|
+
)
|
|
164
|
+
pruned = int(result.get("count") or 0)
|
|
165
|
+
except Exception:
|
|
166
|
+
LOGGER.exception("consolidation prune failed")
|
|
167
|
+
|
|
168
|
+
# v9.6.x additive: graph-layer node merge plan. Always dry-run from
|
|
169
|
+
# this service — graph content changes stay proposal-first; the plan
|
|
170
|
+
# is surfaced so a governed apply path can adopt it later.
|
|
171
|
+
graph_consolidation: Optional[Dict[str, Any]] = None
|
|
172
|
+
proactive = self._proactive()
|
|
173
|
+
if proactive is not None:
|
|
174
|
+
try:
|
|
175
|
+
graph_consolidation = proactive.consolidate_duplicates(
|
|
176
|
+
workspace_id=workspace_id, dry_run=True
|
|
177
|
+
)
|
|
178
|
+
except Exception:
|
|
179
|
+
LOGGER.exception("graph consolidation plan failed")
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
"mode": "applied" if apply else "dry_run",
|
|
183
|
+
"memories_scanned": len(memory_rows),
|
|
184
|
+
"duplicate_memories": duplicate_memory_ids,
|
|
185
|
+
"duplicate_memory_count": len(duplicate_memory_ids),
|
|
186
|
+
"pruned": pruned,
|
|
187
|
+
# Graph edges are reported for review only; consolidation never
|
|
188
|
+
# mutates graph content directly.
|
|
189
|
+
"duplicate_edges": duplicate_edge_ids[:50],
|
|
190
|
+
"duplicate_edge_count": len(duplicate_edge_ids),
|
|
191
|
+
"graph_consolidation": graph_consolidation,
|
|
192
|
+
"generated_at": _now(),
|
|
193
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Windows, sample sizes, and the two readings every surface shares.
|
|
2
|
+
|
|
3
|
+
Leaf module: literals and pure functions only, no service and no store. The
|
|
4
|
+
health report, the insights digest, the garden and the contradiction scan all
|
|
5
|
+
measure against the same windows, which is what makes their numbers
|
|
6
|
+
comparable.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
from datetime import datetime, timezone
|
|
13
|
+
from typing import Any, Optional
|
|
14
|
+
|
|
15
|
+
LOGGER = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
_STALE_DAYS = 45
|
|
18
|
+
_RECENT_DAYS = 7
|
|
19
|
+
_GRAPH_SAMPLE_LIMIT = 800
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _parse_ts(value: Any) -> Optional[datetime]:
|
|
23
|
+
text = str(value or "").strip()
|
|
24
|
+
if not text:
|
|
25
|
+
return None
|
|
26
|
+
try:
|
|
27
|
+
parsed = datetime.fromisoformat(text.replace("Z", "+00:00"))
|
|
28
|
+
except ValueError:
|
|
29
|
+
return None
|
|
30
|
+
if parsed.tzinfo is None:
|
|
31
|
+
parsed = parsed.replace(tzinfo=timezone.utc)
|
|
32
|
+
return parsed
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _no_graph_reason(graph_available: bool) -> str:
|
|
36
|
+
"""Why a graph-derived health dimension has nothing to say.
|
|
37
|
+
|
|
38
|
+
Two different situations that both end in ``status: "unavailable"``: the
|
|
39
|
+
graph could not be read at all, and the graph read fine but holds nothing
|
|
40
|
+
yet. Telling them apart is the difference between "something is broken"
|
|
41
|
+
and "you have not saved anything yet".
|
|
42
|
+
"""
|
|
43
|
+
return (
|
|
44
|
+
"no knowledge saved yet"
|
|
45
|
+
if graph_available
|
|
46
|
+
else "the knowledge graph could not be read"
|
|
47
|
+
)
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""What the Brain would tell you if you asked "what's going on in there?"
|
|
2
|
+
|
|
3
|
+
Three read-only views over the same graph slice, each answering a different
|
|
4
|
+
shape of that question:
|
|
5
|
+
|
|
6
|
+
* :meth:`insights` — the proactive digest: recent growth, trending types, stale
|
|
7
|
+
knowledge, orphans, and questions grounded in real node titles;
|
|
8
|
+
* :meth:`garden_overview` — the same knowledge as four beds a gardener tends
|
|
9
|
+
(new growth, weeds, neglect, load-bearing), because "how healthy is my
|
|
10
|
+
knowledge?" in aggregate is not a question anyone acts on;
|
|
11
|
+
* :meth:`graph_duplicates` / :meth:`quality_report` — the graph layer's own
|
|
12
|
+
proactive scans, surfaced without applying anything.
|
|
13
|
+
|
|
14
|
+
Honest when empty: an unavailable graph produces empty beds and
|
|
15
|
+
``available: False``, never invented plants.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from datetime import datetime, timedelta, timezone
|
|
21
|
+
from typing import Any, Dict, List, Optional
|
|
22
|
+
|
|
23
|
+
from latticeai.core.timeutil import now_iso as _now
|
|
24
|
+
|
|
25
|
+
from ._contract import BrainIntelligenceCore as _Core
|
|
26
|
+
from .constants import _RECENT_DAYS, _STALE_DAYS, LOGGER, _parse_ts
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class BrainDigestMixin(_Core):
|
|
30
|
+
"""Insights, garden, and the graph-layer quality reads."""
|
|
31
|
+
|
|
32
|
+
# ── insights digest ──────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
def insights(
|
|
35
|
+
self, *, user_email: Optional[str] = None, workspace_id: Optional[str] = None
|
|
36
|
+
) -> Dict[str, Any]:
|
|
37
|
+
sample = self._graph_sample(workspace_id=workspace_id)
|
|
38
|
+
nodes, edges = sample["nodes"], sample["edges"]
|
|
39
|
+
now = datetime.now(timezone.utc)
|
|
40
|
+
recent_cutoff = now - timedelta(days=_RECENT_DAYS)
|
|
41
|
+
stale_cutoff = now - timedelta(days=_STALE_DAYS)
|
|
42
|
+
|
|
43
|
+
recent_nodes: List[Dict[str, Any]] = []
|
|
44
|
+
stale_nodes: List[Dict[str, Any]] = []
|
|
45
|
+
type_counts: Dict[str, int] = {}
|
|
46
|
+
for node in nodes:
|
|
47
|
+
node_type = str(node.get("type") or "node")
|
|
48
|
+
ts = _parse_ts(node.get("updated_at"))
|
|
49
|
+
if ts is not None and ts >= recent_cutoff:
|
|
50
|
+
recent_nodes.append(node)
|
|
51
|
+
type_counts[node_type] = type_counts.get(node_type, 0) + 1
|
|
52
|
+
elif ts is not None and ts < stale_cutoff:
|
|
53
|
+
stale_nodes.append(node)
|
|
54
|
+
|
|
55
|
+
connected = set()
|
|
56
|
+
for edge in edges:
|
|
57
|
+
connected.add(str(edge.get("source") or edge.get("from_node") or ""))
|
|
58
|
+
connected.add(str(edge.get("target") or edge.get("to_node") or ""))
|
|
59
|
+
orphans = [n for n in nodes if str(n.get("id")) not in connected]
|
|
60
|
+
|
|
61
|
+
def _slim(node: Dict[str, Any]) -> Dict[str, Any]:
|
|
62
|
+
return {
|
|
63
|
+
"id": node.get("id"),
|
|
64
|
+
"type": node.get("type"),
|
|
65
|
+
"title": str(node.get("title") or "")[:120],
|
|
66
|
+
"updated_at": node.get("updated_at"),
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
trending = sorted(type_counts.items(), key=lambda kv: kv[1], reverse=True)[:5]
|
|
70
|
+
suggested_questions = [
|
|
71
|
+
f"{str(node.get('title') or '').strip()[:60]}에 대해 지금까지 알고 있는 것을 정리해줘"
|
|
72
|
+
for node in recent_nodes[:3]
|
|
73
|
+
if str(node.get("title") or "").strip()
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
"window_days": _RECENT_DAYS,
|
|
78
|
+
"activity": {
|
|
79
|
+
"recent_nodes": len(recent_nodes),
|
|
80
|
+
"recent_samples": [_slim(n) for n in recent_nodes[:8]],
|
|
81
|
+
"trending_types": [{"type": t, "count": c} for t, c in trending],
|
|
82
|
+
},
|
|
83
|
+
"attention": {
|
|
84
|
+
"stale_nodes": len(stale_nodes),
|
|
85
|
+
"stale_samples": [_slim(n) for n in stale_nodes[:8]],
|
|
86
|
+
"orphan_nodes": len(orphans),
|
|
87
|
+
"orphan_samples": [_slim(n) for n in orphans[:8]],
|
|
88
|
+
},
|
|
89
|
+
"suggested_questions": suggested_questions,
|
|
90
|
+
"graph_available": sample["available"],
|
|
91
|
+
"generated_at": _now(),
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
def garden_overview(
|
|
95
|
+
self, *, user_email: Optional[str] = None, workspace_id: Optional[str] = None,
|
|
96
|
+
limit: int = 8,
|
|
97
|
+
) -> Dict[str, Any]:
|
|
98
|
+
"""The knowledge garden in four beds (v9.9.7).
|
|
99
|
+
|
|
100
|
+
Living Brain answers "how healthy is my knowledge?" in aggregate.
|
|
101
|
+
A gardener asks four concrete questions instead, and this answers all
|
|
102
|
+
four from one workspace-scoped graph sample plus the memory tier:
|
|
103
|
+
|
|
104
|
+
* **recent** — what came in lately (the garden's new growth);
|
|
105
|
+
* **contradictions** — what disagrees with itself (needs weeding);
|
|
106
|
+
* **stale** — what has not been touched in a long time;
|
|
107
|
+
* **frequent** — what the rest of the graph leans on most (by degree).
|
|
108
|
+
|
|
109
|
+
Read-only and honest: when the graph is unavailable every bed is empty
|
|
110
|
+
and ``available`` is false — the view never invents plants.
|
|
111
|
+
"""
|
|
112
|
+
# Explicit 0 clamps to 1 — `limit or 8` would silently re-expand it.
|
|
113
|
+
try:
|
|
114
|
+
limit = max(1, min(int(limit), 50))
|
|
115
|
+
except (TypeError, ValueError):
|
|
116
|
+
limit = 8
|
|
117
|
+
sample = self._graph_sample(workspace_id=workspace_id)
|
|
118
|
+
nodes, edges = sample["nodes"], sample["edges"]
|
|
119
|
+
now = datetime.now(timezone.utc)
|
|
120
|
+
recent_cutoff = now - timedelta(days=_RECENT_DAYS)
|
|
121
|
+
stale_cutoff = now - timedelta(days=_STALE_DAYS)
|
|
122
|
+
|
|
123
|
+
def _slim(node: Dict[str, Any], **extra: Any) -> Dict[str, Any]:
|
|
124
|
+
return {
|
|
125
|
+
"id": node.get("id"),
|
|
126
|
+
"type": node.get("type"),
|
|
127
|
+
"title": str(node.get("title") or "")[:120],
|
|
128
|
+
"updated_at": node.get("updated_at"),
|
|
129
|
+
**extra,
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
recent: List[Dict[str, Any]] = []
|
|
133
|
+
stale: List[Dict[str, Any]] = []
|
|
134
|
+
for node in nodes:
|
|
135
|
+
# Chunks are retrieval plumbing, not knowledge a gardener tends.
|
|
136
|
+
if str(node.get("type") or "") == "Chunk":
|
|
137
|
+
continue
|
|
138
|
+
ts = _parse_ts(node.get("updated_at"))
|
|
139
|
+
if ts is None:
|
|
140
|
+
continue
|
|
141
|
+
if ts >= recent_cutoff:
|
|
142
|
+
recent.append(node)
|
|
143
|
+
elif ts < stale_cutoff:
|
|
144
|
+
stale.append(node)
|
|
145
|
+
recent.sort(key=lambda n: str(n.get("updated_at") or ""), reverse=True)
|
|
146
|
+
stale.sort(key=lambda n: str(n.get("updated_at") or ""))
|
|
147
|
+
|
|
148
|
+
# "Frequent" is degree, not a guess: how many relations actually point
|
|
149
|
+
# at a node. Chunks are retrieval plumbing, never garden plants.
|
|
150
|
+
degree: Dict[str, int] = {}
|
|
151
|
+
for edge in edges:
|
|
152
|
+
for key in ("source", "target"):
|
|
153
|
+
node_id = str(edge.get(key) or "")
|
|
154
|
+
if node_id:
|
|
155
|
+
degree[node_id] = degree.get(node_id, 0) + 1
|
|
156
|
+
by_id = {str(node.get("id")): node for node in nodes}
|
|
157
|
+
frequent = [
|
|
158
|
+
_slim(by_id[node_id], degree=count)
|
|
159
|
+
for node_id, count in sorted(degree.items(), key=lambda kv: kv[1], reverse=True)
|
|
160
|
+
if node_id in by_id and str(by_id[node_id].get("type") or "") != "Chunk"
|
|
161
|
+
][:limit]
|
|
162
|
+
|
|
163
|
+
contradiction_items: List[Dict[str, Any]] = []
|
|
164
|
+
contradiction_count = 0
|
|
165
|
+
try:
|
|
166
|
+
found = self.contradictions(user_email=user_email, workspace_id=workspace_id)
|
|
167
|
+
items = found.get("items") if isinstance(found, dict) else None
|
|
168
|
+
if isinstance(items, list):
|
|
169
|
+
contradiction_count = len(items)
|
|
170
|
+
contradiction_items = items[:limit]
|
|
171
|
+
except Exception: # noqa: BLE001 — one empty bed, never a broken view
|
|
172
|
+
LOGGER.exception("garden overview contradictions failed")
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
"available": sample["available"],
|
|
176
|
+
"window_days": _RECENT_DAYS,
|
|
177
|
+
"stale_threshold_days": _STALE_DAYS,
|
|
178
|
+
"beds": {
|
|
179
|
+
"recent": {
|
|
180
|
+
"count": len(recent),
|
|
181
|
+
"items": [_slim(node) for node in recent[:limit]],
|
|
182
|
+
},
|
|
183
|
+
"contradictions": {
|
|
184
|
+
"count": contradiction_count,
|
|
185
|
+
"items": contradiction_items,
|
|
186
|
+
},
|
|
187
|
+
"stale": {
|
|
188
|
+
"count": len(stale),
|
|
189
|
+
"items": [_slim(node) for node in stale[:limit]],
|
|
190
|
+
},
|
|
191
|
+
"frequent": {
|
|
192
|
+
"count": len(frequent),
|
|
193
|
+
"items": frequent,
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
"generated_at": _now(),
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
# ── graph-layer proactive quality (v9.6.x) ───────────────────────────
|
|
200
|
+
|
|
201
|
+
def graph_duplicates(
|
|
202
|
+
self, *, user_email: Optional[str] = None, workspace_id: Optional[str] = None
|
|
203
|
+
) -> Dict[str, Any]:
|
|
204
|
+
"""Duplicate graph nodes (exact groups + near pairs) — read only."""
|
|
205
|
+
proactive = self._proactive()
|
|
206
|
+
if proactive is None:
|
|
207
|
+
return {
|
|
208
|
+
"available": False,
|
|
209
|
+
"exact_groups": [],
|
|
210
|
+
"near_pairs": [],
|
|
211
|
+
"exact_duplicate_nodes": 0,
|
|
212
|
+
"nodes_scanned": 0,
|
|
213
|
+
"generated_at": _now(),
|
|
214
|
+
}
|
|
215
|
+
try:
|
|
216
|
+
result = dict(proactive.find_duplicates(workspace_id=workspace_id))
|
|
217
|
+
except Exception as exc:
|
|
218
|
+
LOGGER.exception("graph duplicates scan failed")
|
|
219
|
+
return {
|
|
220
|
+
"available": False,
|
|
221
|
+
"error": str(exc),
|
|
222
|
+
"exact_groups": [],
|
|
223
|
+
"near_pairs": [],
|
|
224
|
+
"exact_duplicate_nodes": 0,
|
|
225
|
+
"nodes_scanned": 0,
|
|
226
|
+
"generated_at": _now(),
|
|
227
|
+
}
|
|
228
|
+
result["available"] = True
|
|
229
|
+
result["generated_at"] = _now()
|
|
230
|
+
return result
|
|
231
|
+
|
|
232
|
+
def quality_report(
|
|
233
|
+
self, *, user_email: Optional[str] = None, workspace_id: Optional[str] = None
|
|
234
|
+
) -> Dict[str, Any]:
|
|
235
|
+
"""Combined graph quality report: duplicates, contradictions, stale
|
|
236
|
+
nodes, edge quality — one workspace-scoped graph sample."""
|
|
237
|
+
proactive = self._proactive()
|
|
238
|
+
if proactive is None:
|
|
239
|
+
return {"available": False, "generated_at": _now()}
|
|
240
|
+
try:
|
|
241
|
+
result = dict(proactive.quality_report(workspace_id=workspace_id))
|
|
242
|
+
except Exception as exc:
|
|
243
|
+
LOGGER.exception("graph quality report failed")
|
|
244
|
+
return {"available": False, "error": str(exc), "generated_at": _now()}
|
|
245
|
+
result["available"] = True
|
|
246
|
+
# v11.1.0: decay is part of quality, and "the Brain is tidying up" is a
|
|
247
|
+
# state the user is entitled to see rather than a background surprise.
|
|
248
|
+
importance = self.importance_report(
|
|
249
|
+
user_email=user_email, workspace_id=workspace_id
|
|
250
|
+
)
|
|
251
|
+
candidates = len(importance.get("candidates") or [])
|
|
252
|
+
result["importance"] = importance
|
|
253
|
+
result["tidying"] = bool(importance.get("available")) and candidates > 0
|
|
254
|
+
summary = dict(result.get("summary") or {})
|
|
255
|
+
summary["consolidation_candidates"] = candidates
|
|
256
|
+
result["summary"] = summary
|
|
257
|
+
result["generated_at"] = _now()
|
|
258
|
+
return result
|