ltcai 11.1.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 +47 -54
- package/docs/CHANGELOG.md +94 -0
- package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
- package/docs/DEVELOPMENT.md +1 -1
- package/docs/FEATURE_AUDIT_v11.2.0.md +393 -0
- package/docs/LAYOUT_REBUILD_SPEC.md +9 -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/architecture.md +6 -2
- 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/gates.py +125 -0
- 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/fusion.py +35 -4
- 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} +81 -485
- 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/graph/schema.py +9 -0
- package/lattice_brain/graph/store.py +9 -0
- package/lattice_brain/graph/vector_index/selector.py +32 -2
- 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.py → multimodal/images.py} +22 -262
- 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/sharing.py +714 -0
- package/lattice_brain/runtime/__init__.py +1 -1
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/lattice_brain/sealed_box.py +244 -0
- package/lattice_brain/synthesis.py +24 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/brain_intelligence.py +4 -0
- package/latticeai/api/chat.py +11 -0
- package/latticeai/api/chat_helpers.py +16 -3
- package/latticeai/api/chat_hybrid.py +32 -1
- package/latticeai/api/chronicle.py +63 -0
- package/latticeai/api/features.py +70 -0
- package/latticeai/api/local_files.py +102 -0
- package/latticeai/api/portability.py +39 -4
- package/latticeai/api/review_queue.py +126 -0
- package/latticeai/api/search.py +16 -2
- 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/config.py +4 -1
- package/latticeai/core/context_builder.py +6 -3
- 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 +152 -0
- package/latticeai/core/model_compat.py +73 -2
- 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/model_providers.py +12 -4
- 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/chat_wiring.py +4 -0
- package/latticeai/runtime/feature_toggle_wiring.py +163 -0
- package/latticeai/runtime/router_registration.py +11 -0
- package/latticeai/runtime/runtime_context.py +1 -0
- package/latticeai/services/app_context.py +8 -0
- package/latticeai/services/architecture_readiness.py +48 -19
- package/latticeai/services/automation_intelligence.py +22 -2
- 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/command_center.py +10 -4
- package/latticeai/services/feature_toggles.py +502 -0
- package/latticeai/services/folder_watch.py +122 -1
- package/latticeai/services/hybrid_chat.py +56 -5
- package/latticeai/services/interop_bridges.py +978 -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_capability_registry.py +434 -261
- package/latticeai/services/model_catalog.py +95 -61
- package/latticeai/services/model_recommendation.py +18 -11
- 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/multimodal_ports.py +26 -1
- package/latticeai/services/obsidian_bridge.py +16 -25
- package/latticeai/services/product_readiness.py +15 -7
- package/latticeai/services/search_service.py +149 -2
- package/latticeai/services/tool_dispatch.py +4 -0
- package/latticeai/setup/auto_setup.py +27 -30
- 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 +2 -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 +44 -2
- package/scripts/verify_hf_model_registry.py +253 -218
- 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-CzCsI1mi.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-BXMWIK_2.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-4BvN07ux.js → bot-IM_E_Y12.js} +1 -1
- package/static/app/assets/brain-Ci1CkWjM.js +1 -0
- package/static/app/assets/{button-CDjtnAoU.js → button-COwyqfHM.js} +1 -1
- package/static/app/assets/circle-check-DfInj-qD.js +1 -0
- package/static/app/assets/{circle-pause-D_RMn7tp.js → circle-pause-DEM4A1Y5.js} +1 -1
- package/static/app/assets/{circle-play-B5OpB8ae.js → circle-play-C9djDuLd.js} +1 -1
- package/static/app/assets/{cpu-BIlWInHf.js → cpu-DFdo1gw-.js} +1 -1
- package/static/app/assets/{download-BtjXfL3z.js → download-SnJL6oqk.js} +1 -1
- package/static/app/assets/{folder-open-DefMpxI2.js → folder-open-CqZeDkjE.js} +1 -1
- package/static/app/assets/{hard-drive-BQ8NZVkw.js → hard-drive-j1jJXYYf.js} +1 -1
- package/static/app/assets/{index-vtEfYvQY.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-BqZ5tsgL.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-D5zg_0fY.js → share-2-CVtZ_ewX.js} +1 -1
- package/static/app/assets/{shield-alert-B5pZzkUb.js → shield-alert-CBi2GNWM.js} +1 -1
- package/static/app/assets/{textarea-nEVIweKY.js → textarea-DNMpB5ih.js} +1 -1
- package/static/app/assets/{useFocusTrap-Cm99AHlz.js → useFocusTrap-C83t3GXF.js} +1 -1
- package/static/app/assets/useMutation-DtbJDoyz.js +1 -0
- package/static/app/assets/{useQuery-Dm__N6bL.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 -1377
- package/lattice_brain/portability.py +0 -1210
- package/latticeai/core/agent.py +0 -1412
- 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 -1422
- package/latticeai/services/brain_intelligence.py +0 -967
- package/latticeai/services/memory_service.py +0 -1177
- package/latticeai/services/model_runtime.py +0 -1281
- package/latticeai/setup/wizard.py +0 -1277
- package/static/app/assets/Act-D0HWqtn0.js +0 -1
- package/static/app/assets/AdminConsole-D-QDW-A4.js +0 -1
- package/static/app/assets/BrainHome-Btns-_TA.js +0 -2
- package/static/app/assets/BrainSignals-2dHQNkns.js +0 -1
- package/static/app/assets/Capture-CT8v1StE.js +0 -1
- package/static/app/assets/CommandPalette-DoLXC2KH.js +0 -1
- package/static/app/assets/Library-DDoxFE5c.js +0 -1
- package/static/app/assets/ProductFlow-DOYf7JIs.js +0 -1
- package/static/app/assets/ReviewCard-COQsqidK.js +0 -3
- package/static/app/assets/System-BRllvYXd.js +0 -1
- package/static/app/assets/arrow-left-DnyMzss-.js +0 -1
- package/static/app/assets/brain-uMb_5hnO.js +0 -1
- package/static/app/assets/index-0AvoEBzJ.js +0 -10
- package/static/app/assets/input-B_5ZJ9oy.js +0 -1
- package/static/app/assets/primitives-CVwew78r.js +0 -1
- package/static/app/assets/search-DkhnOKZt.js +0 -1
- package/static/app/assets/utils-DcDMoZIe.js +0 -4
- package/static/app/assets/workspace-LtRRSKTf.js +0 -1
|
@@ -76,6 +76,14 @@ class AppContext:
|
|
|
76
76
|
workspace_graph: Optional[Callable[[], Any]] = None
|
|
77
77
|
graph_stats: Optional[Callable[[], dict]] = None
|
|
78
78
|
|
|
79
|
+
# ── review center ─────────────────────────────────────────────────────
|
|
80
|
+
# ``ReviewQueueService``, reached through a provider like
|
|
81
|
+
# ``workspace_graph`` above: the queue is built in a later runtime phase
|
|
82
|
+
# than this context, so a value captured here would be ``None`` forever.
|
|
83
|
+
# Call it per request; ``None`` means no Review Center is wired (tests,
|
|
84
|
+
# headless helpers) and the callers stage nothing rather than writing.
|
|
85
|
+
review_queue: Optional[Callable[[], Any]] = None
|
|
86
|
+
|
|
79
87
|
# ── workspace payload providers / skills ──────────────────────────────
|
|
80
88
|
workspace_models: Optional[Callable[[], dict]] = None
|
|
81
89
|
workspace_settings: Optional[Callable[[], dict]] = None
|
|
@@ -16,7 +16,7 @@ from typing import Any, Dict, List
|
|
|
16
16
|
|
|
17
17
|
from latticeai.core.legacy_compatibility import legacy_shim_report
|
|
18
18
|
|
|
19
|
-
ARCHITECTURE_VERSION_TARGET = "11.
|
|
19
|
+
ARCHITECTURE_VERSION_TARGET = "11.4.0"
|
|
20
20
|
|
|
21
21
|
PREFERRED_REFACTORING_ORDER = [
|
|
22
22
|
"agent-runtime",
|
|
@@ -53,6 +53,19 @@ def _symbol_exists(dotted: str) -> bool:
|
|
|
53
53
|
return False
|
|
54
54
|
|
|
55
55
|
|
|
56
|
+
def _package_sources(root: Path, relative_package: str) -> List[str]:
|
|
57
|
+
"""Every ``*.py`` in a package, as repo-relative paths.
|
|
58
|
+
|
|
59
|
+
Returns the missing marker path when the package is absent, so a gate that
|
|
60
|
+
scans a package fails the same way it would for a deleted single file.
|
|
61
|
+
"""
|
|
62
|
+
directory = root / relative_package
|
|
63
|
+
sources = sorted(directory.glob("*.py")) if directory.is_dir() else []
|
|
64
|
+
if not sources:
|
|
65
|
+
return [f"{relative_package}/__init__.py"]
|
|
66
|
+
return [str(path.relative_to(root)) for path in sources]
|
|
67
|
+
|
|
68
|
+
|
|
56
69
|
def _forbidden_patterns(root: Path, relative_path: str, patterns: List[str]) -> List[str]:
|
|
57
70
|
path = root / relative_path
|
|
58
71
|
try:
|
|
@@ -71,18 +84,26 @@ def architecture_readiness(root: Path | None = None) -> Dict[str, Any]:
|
|
|
71
84
|
"latticeai/app_factory.py",
|
|
72
85
|
["build_runtime_namespace(locals", "dict(locals())"],
|
|
73
86
|
)
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
87
|
+
# v11.3.0: model_runtime.py became a package. The ambient-state patterns
|
|
88
|
+
# below could reappear in any of its submodules, so every file in the
|
|
89
|
+
# package is scanned — checking only __init__.py would leave the gate
|
|
90
|
+
# blind one directory down.
|
|
91
|
+
model_runtime_forbidden = [
|
|
92
|
+
finding
|
|
93
|
+
for relative_path in _package_sources(root, "latticeai/services/model_runtime")
|
|
94
|
+
for finding in _forbidden_patterns(
|
|
95
|
+
root,
|
|
96
|
+
relative_path,
|
|
97
|
+
[
|
|
98
|
+
"def _sync_globals",
|
|
99
|
+
"global router",
|
|
100
|
+
"STATE = ModelRuntimeState",
|
|
101
|
+
"_STATE_EXPORTS",
|
|
102
|
+
"def __getattr__(name:",
|
|
103
|
+
"from fastapi import HTTPException",
|
|
104
|
+
],
|
|
105
|
+
)
|
|
106
|
+
]
|
|
86
107
|
model_runtime_forbidden.extend(
|
|
87
108
|
_forbidden_patterns(
|
|
88
109
|
root,
|
|
@@ -97,11 +118,19 @@ def architecture_readiness(root: Path | None = None) -> Dict[str, Any]:
|
|
|
97
118
|
["from fastapi import HTTPException", "raise HTTPException"],
|
|
98
119
|
)
|
|
99
120
|
)
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
121
|
+
# v11.3.0: the single-agent loop is a package. The compatibility alias
|
|
122
|
+
# could reappear in any of its submodules, so every file in the package is
|
|
123
|
+
# scanned — checking only __init__.py would leave the gate blind one
|
|
124
|
+
# directory down.
|
|
125
|
+
agent_alias_forbidden = [
|
|
126
|
+
finding
|
|
127
|
+
for relative_path in _package_sources(root, "latticeai/core/agent")
|
|
128
|
+
for finding in _forbidden_patterns(
|
|
129
|
+
root,
|
|
130
|
+
relative_path,
|
|
131
|
+
["AgentRuntime = SingleAgentRuntime"],
|
|
132
|
+
)
|
|
133
|
+
]
|
|
105
134
|
|
|
106
135
|
gates = [
|
|
107
136
|
ArchitectureGate(
|
|
@@ -180,7 +209,7 @@ def architecture_readiness(root: Path | None = None) -> Dict[str, Any]:
|
|
|
180
209
|
evidence=[
|
|
181
210
|
"frontend/src/components/ProductFlow.tsx Wake Brain entry",
|
|
182
211
|
"frontend/src/features/brain/BrainHome.tsx memory rings",
|
|
183
|
-
"tests/visual/v3.spec.js first-run and Brain depth coverage",
|
|
212
|
+
"tests/visual/v3.home.spec.js first-run and Brain depth coverage",
|
|
184
213
|
],
|
|
185
214
|
),
|
|
186
215
|
ArchitectureGate(
|
|
@@ -55,6 +55,13 @@ _SIGNATURE_SIMILARITY = 0.6
|
|
|
55
55
|
_MIN_SUGGESTION_CONFIDENCE = 0.35
|
|
56
56
|
_LOW_CONFIDENCE_THRESHOLD = 0.5
|
|
57
57
|
_KG_GROUNDING_LIMIT = 5
|
|
58
|
+
#: Charged when the graph *was* consulted and found nothing related. v11.2.0
|
|
59
|
+
#: fixes a dead policy: the floor above was unreachable for recurring questions
|
|
60
|
+
#: (the cheapest possible question — asked twice, one phrasing, no recipe —
|
|
61
|
+
#: scored 0.475), so the suppression branch could only ever be exercised by a
|
|
62
|
+
#: test that moved the constant. Absence of grounding is real evidence, and
|
|
63
|
+
#: charging for it is what makes the floor mean something again.
|
|
64
|
+
_UNGROUNDED_PENALTY = 0.15
|
|
58
65
|
|
|
59
66
|
|
|
60
67
|
def _question_confidence(
|
|
@@ -67,7 +74,13 @@ def _question_confidence(
|
|
|
67
74
|
|
|
68
75
|
Evidence factors: how often the question repeats, how many distinct
|
|
69
76
|
phrasings exist, whether it maps onto a known starter-recipe intent, and
|
|
70
|
-
|
|
77
|
+
what the Brain knows about it.
|
|
78
|
+
|
|
79
|
+
That last factor is three-valued on purpose. ``None`` means the graph could
|
|
80
|
+
not be consulted — unknown, so nothing is added or taken away. A positive
|
|
81
|
+
count is supporting evidence. **Zero** is not the same as unknown: the
|
|
82
|
+
graph was asked, and it knows nothing about this question, which is a
|
|
83
|
+
reason to be less sure rather than a reason to be neutral.
|
|
71
84
|
"""
|
|
72
85
|
score = 0.3 + 0.5 * min(1.0, (int(count) - 1) / 4)
|
|
73
86
|
score += min(0.15, 0.05 * len(examples or []))
|
|
@@ -75,13 +88,16 @@ def _question_confidence(
|
|
|
75
88
|
score += 0.15
|
|
76
89
|
if kg_related:
|
|
77
90
|
score += min(0.2, 0.05 * int(kg_related))
|
|
91
|
+
elif kg_related == 0:
|
|
92
|
+
score -= _UNGROUNDED_PENALTY
|
|
78
93
|
factors = {
|
|
79
94
|
"repeat_count": int(count),
|
|
80
95
|
"distinct_examples": len(examples or []),
|
|
81
96
|
"intent_match": bool(recipe_id),
|
|
82
97
|
"kg_related_nodes": kg_related,
|
|
98
|
+
"kg_grounded": None if kg_related is None else bool(kg_related),
|
|
83
99
|
}
|
|
84
|
-
return round(min(1.0, score), 2), factors
|
|
100
|
+
return round(max(0.0, min(1.0, score)), 2), factors
|
|
85
101
|
|
|
86
102
|
|
|
87
103
|
def _source_confidence(indexed: int, watch_enabled: bool) -> tuple:
|
|
@@ -357,6 +373,10 @@ class AutomationIntelligenceService:
|
|
|
357
373
|
pattern["count"], pattern["examples"], recipe_id, kg_related
|
|
358
374
|
)
|
|
359
375
|
if confidence < _MIN_SUGGESTION_CONFIDENCE:
|
|
376
|
+
# Asked a couple of times, phrased one way, matching no recipe,
|
|
377
|
+
# and the Brain knows nothing about it — not enough to propose
|
|
378
|
+
# automating. Counted, so the surface can say how many were held
|
|
379
|
+
# back rather than silently showing fewer.
|
|
360
380
|
suppressed_low_confidence += 1
|
|
361
381
|
continue
|
|
362
382
|
if recipe_id:
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Proactive Brain Intelligence service (v9.3.0).
|
|
2
|
+
|
|
3
|
+
The Brain graduates from a passive store to an active steward of its own
|
|
4
|
+
knowledge. This service wires the previously dormant quality layer
|
|
5
|
+
(:mod:`lattice_brain.quality` — dedupe, merge, conflict and temporal
|
|
6
|
+
contradiction detection, retention) into router-facing capabilities:
|
|
7
|
+
|
|
8
|
+
* **health_report** — scored diagnosis of the Brain across freshness,
|
|
9
|
+
connectivity, embedding coverage, and contradiction pressure, with
|
|
10
|
+
recommended next actions. Every number is read from the live stores;
|
|
11
|
+
a missing store degrades the dimension to ``unavailable``, never a guess.
|
|
12
|
+
* **insights** — a proactive digest: recent knowledge growth, most active
|
|
13
|
+
types, stale knowledge, orphan (disconnected) nodes, and suggested
|
|
14
|
+
questions grounded in real node titles.
|
|
15
|
+
* **contradictions** — surfaced conflicts across workspace memories
|
|
16
|
+
(negation/preference conflicts, temporal contradictions) plus explicit
|
|
17
|
+
CONTRADICTS edges already recorded in the graph.
|
|
18
|
+
* **consolidate** — duplicate-memory and duplicate-edge detection. Dry-run
|
|
19
|
+
by default (consent-first, like every Brain automation); ``apply=True``
|
|
20
|
+
prunes only exact duplicate workspace memories through the audited
|
|
21
|
+
MemoryService path and never touches graph content.
|
|
22
|
+
|
|
23
|
+
Pure service: no FastAPI, no globals. Collaborators are injected.
|
|
24
|
+
|
|
25
|
+
Split into cohesive submodules in v11.3.0 (no behaviour change): ``constants``
|
|
26
|
+
(windows + the two shared readings), ``proposals`` (the review-queue path and
|
|
27
|
+
the proactive brief), ``sampling`` (the shared graph slice and memory read),
|
|
28
|
+
``health`` (the scored diagnosis and vector freshness), ``digest`` (insights,
|
|
29
|
+
garden, graph-layer quality reads), ``consistency`` (contradiction and
|
|
30
|
+
consolidation scans), and ``service`` (the composed class). This module
|
|
31
|
+
re-exports every name the single file exposed, so
|
|
32
|
+
``latticeai.services.brain_intelligence.X`` keeps working.
|
|
33
|
+
|
|
34
|
+
Stubbing note: rebinding one of these *here* changes only this module's name.
|
|
35
|
+
The submodule that calls it holds its own reference, so a test standing in for
|
|
36
|
+
``LOGGER`` or ``_parse_ts`` patches the submodule that uses it.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
from __future__ import annotations
|
|
40
|
+
|
|
41
|
+
from lattice_brain.quality import GraphEdgeQualityManager as GraphEdgeQualityManager
|
|
42
|
+
from lattice_brain.quality import MemoryQualityManager as MemoryQualityManager
|
|
43
|
+
|
|
44
|
+
# noqa on the next line, not a redundant alias: the single file exposed this
|
|
45
|
+
# name *renamed* (``brain_intelligence._now``), and reproducing that surface
|
|
46
|
+
# exactly is the point — importing it as ``now_iso`` would add a name the
|
|
47
|
+
# module never had.
|
|
48
|
+
from latticeai.core.timeutil import now_iso as _now # noqa: F401
|
|
49
|
+
|
|
50
|
+
from .constants import _GRAPH_SAMPLE_LIMIT as _GRAPH_SAMPLE_LIMIT
|
|
51
|
+
from .constants import _RECENT_DAYS as _RECENT_DAYS
|
|
52
|
+
from .constants import _STALE_DAYS as _STALE_DAYS
|
|
53
|
+
from .constants import LOGGER as LOGGER
|
|
54
|
+
from .constants import _no_graph_reason as _no_graph_reason
|
|
55
|
+
from .constants import _parse_ts as _parse_ts
|
|
56
|
+
from .service import BrainIntelligenceService as BrainIntelligenceService
|
|
57
|
+
|
|
58
|
+
__all__ = ["BrainIntelligenceService"]
|
|
@@ -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
|
+
)
|