ltcai 10.2.0 → 10.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 +40 -44
- package/docs/CHANGELOG.md +125 -0
- package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
- package/docs/DEVELOPMENT.md +1 -1
- package/docs/MYPY_BACKLOG.md +65 -0
- package/docs/ONBOARDING.md +1 -1
- package/docs/OPERATIONS.md +1 -1
- package/docs/SURFACE_PARITY.md +33 -6
- package/docs/TRUST_MODEL.md +1 -1
- package/docs/WHY_LATTICE.md +1 -1
- package/docs/kg-schema.md +1 -1
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/context.py +23 -3
- package/lattice_brain/graph/_kg_common.py +164 -9
- package/lattice_brain/graph/_kg_contract.py +223 -0
- package/lattice_brain/graph/_kg_fsutil.py +2 -2
- package/lattice_brain/graph/curator.py +4 -4
- package/lattice_brain/graph/discovery.py +12 -1
- package/lattice_brain/graph/discovery_index.py +12 -1
- package/lattice_brain/graph/documents.py +17 -2
- package/lattice_brain/graph/ingest.py +12 -1
- package/lattice_brain/graph/proactive.py +9 -5
- package/lattice_brain/graph/projection.py +12 -1
- package/lattice_brain/graph/provenance.py +12 -1
- package/lattice_brain/graph/retrieval.py +12 -1
- package/lattice_brain/graph/retrieval_docgen.py +12 -1
- package/lattice_brain/graph/retrieval_reads.py +12 -1
- package/lattice_brain/graph/retrieval_vector.py +12 -1
- package/lattice_brain/graph/schema.py +6 -2
- package/lattice_brain/graph/write_master.py +12 -1
- package/lattice_brain/ingestion.py +1 -1
- package/lattice_brain/quality.py +5 -5
- package/lattice_brain/runtime/agent_runtime.py +7 -2
- package/lattice_brain/runtime/contracts.py +2 -1
- package/lattice_brain/runtime/hooks.py +1 -1
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/lattice_brain/workflow.py +10 -7
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/admin.py +2 -2
- package/latticeai/api/chat.py +19 -15
- package/latticeai/api/chat_agent_http.py +19 -7
- package/latticeai/api/chat_helpers.py +9 -8
- package/latticeai/api/chat_stream.py +2 -2
- package/latticeai/api/computer_use.py +2 -2
- package/latticeai/api/hooks.py +4 -2
- package/latticeai/api/local_files.py +8 -4
- package/latticeai/api/mcp.py +2 -2
- package/latticeai/api/models.py +5 -5
- package/latticeai/api/permissions.py +2 -2
- package/latticeai/api/search.py +5 -3
- package/latticeai/api/security_dashboard.py +3 -5
- package/latticeai/api/setup.py +3 -3
- package/latticeai/api/static_routes.py +7 -2
- package/latticeai/api/tools.py +9 -4
- package/latticeai/api/workspace.py +25 -22
- package/latticeai/app_factory.py +132 -1346
- package/latticeai/cli/entrypoint.py +1 -1
- package/latticeai/core/agent.py +28 -14
- package/latticeai/core/audit.py +5 -3
- package/latticeai/core/embedding_providers.py +19 -6
- package/latticeai/core/legacy_compatibility.py +1 -1
- package/latticeai/core/marketplace.py +2 -2
- package/latticeai/core/mcp_catalog.py +3 -1
- package/latticeai/core/model_compat.py +1 -1
- package/latticeai/core/plugins.py +1 -1
- package/latticeai/core/project_sessions.py +1 -1
- package/latticeai/core/run_explain.py +6 -4
- package/latticeai/core/workspace_os.py +0 -4
- package/latticeai/core/workspace_os_constants.py +1 -1
- package/latticeai/core/workspace_os_state.py +1 -1
- package/latticeai/core/workspace_snapshots.py +3 -3
- package/latticeai/integrations/telegram_bot.py +52 -3
- package/latticeai/models/router.py +61 -34
- package/latticeai/runtime/access_runtime.py +3 -1
- package/latticeai/runtime/build_phases.py +1417 -0
- package/latticeai/runtime/history_writer.py +138 -0
- package/latticeai/runtime/review_wiring.py +6 -1
- package/latticeai/runtime/runtime_context.py +276 -0
- package/latticeai/runtime/sso_config_runtime.py +1 -1
- package/latticeai/runtime/stages.py +8 -2
- package/latticeai/services/app_context.py +23 -0
- package/latticeai/services/architecture_readiness.py +1 -1
- package/latticeai/services/brain_intelligence.py +4 -2
- package/latticeai/services/cloud_streaming.py +5 -3
- package/latticeai/services/command_center.py +1 -1
- package/latticeai/services/evidence_actions.py +2 -1
- package/latticeai/services/hybrid_chat.py +1 -1
- package/latticeai/services/local_knowledge.py +25 -9
- package/latticeai/services/memory_service.py +8 -4
- package/latticeai/services/model_catalog.py +12 -12
- package/latticeai/services/model_engines.py +15 -17
- package/latticeai/services/model_loading.py +1 -1
- package/latticeai/services/model_runtime.py +31 -33
- package/latticeai/services/multimodal_streaming.py +10 -4
- package/latticeai/services/openai_compatible_adapter.py +4 -3
- package/latticeai/services/product_readiness.py +1 -1
- package/latticeai/services/run_executor.py +12 -3
- package/latticeai/services/search_service.py +1 -1
- package/latticeai/services/triggers.py +1 -1
- package/latticeai/setup/auto_setup.py +1 -1
- package/latticeai/setup/wizard.py +12 -3
- package/latticeai/tools/computer.py +3 -1
- package/latticeai/tools/documents.py +2 -2
- package/package.json +4 -2
- package/scripts/check_current_release_docs.mjs +1 -1
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/tauri.conf.json +1 -1
- package/static/app/asset-manifest.json +37 -37
- package/static/app/assets/{Act-CbdGD-2i.js → Act-BK__Y5VE.js} +1 -1
- package/static/app/assets/{AdminConsole-LgCkXpnf.js → AdminConsole-ZrTyN8_F.js} +1 -1
- package/static/app/assets/{Brain-CoPGJI1L.js → Brain-CYHDXpPB.js} +1 -1
- package/static/app/assets/{BrainHome-gVnaxSwp.js → BrainHome-bbJulb5g.js} +1 -1
- package/static/app/assets/{BrainSignals-ChxAYHtj.js → BrainSignals-CesSPtUP.js} +1 -1
- package/static/app/assets/{Capture-DBtgkHZg.js → Capture-p5upXmz2.js} +1 -1
- package/static/app/assets/{CommandPalette-Ovtv5I0Y.js → CommandPalette-0zkyC0RY.js} +1 -1
- package/static/app/assets/Library-Dt-vABI2.js +1 -0
- package/static/app/assets/{LivingBrain-CNz-6NSd.js → LivingBrain-Bz-h6LBH.js} +1 -1
- package/static/app/assets/{ProductFlow-ePX-Y73J.js → ProductFlow-DAZ7DJCE.js} +1 -1
- package/static/app/assets/{ReviewCard-C4mpvkwH.js → ReviewCard-Bv0WGbY6.js} +1 -1
- package/static/app/assets/{System-BvWNK1zc.js → System-BhrJQSD_.js} +1 -1
- package/static/app/assets/{activity-CiauPV_3.js → activity-AolpZJCH.js} +1 -1
- package/static/app/assets/{bot-Dwct-Q1x.js → bot-CsVsT2nd.js} +1 -1
- package/static/app/assets/{brain-BKDW1F0T.js → brain-vuppco5y.js} +1 -1
- package/static/app/assets/{button-CCB9Kuw7.js → button-1BP1PJZ3.js} +1 -1
- package/static/app/assets/{circle-pause-DNa8WHC5.js → circle-pause-5paeCOtq.js} +1 -1
- package/static/app/assets/{circle-play-8jmxn5mf.js → circle-play-By8mFYRV.js} +1 -1
- package/static/app/assets/{cpu-mxvF3V1I.js → cpu-DvUQxdKe.js} +1 -1
- package/static/app/assets/{download--SmCcx0p.js → download-CFqYG90a.js} +1 -1
- package/static/app/assets/{folder-open-D4Wy5roo.js → folder-open-CVSnp-Dp.js} +1 -1
- package/static/app/assets/{hard-drive-BwQcSPlS.js → hard-drive-Ckr4Vd6-.js} +1 -1
- package/static/app/assets/{index-DDV2YZwM.js → index--0zKF5Ow.js} +3 -3
- package/static/app/assets/index-CEkL4_M-.css +2 -0
- package/static/app/assets/{input-DFhSjmLS.js → input-D-JKquXU.js} +1 -1
- package/static/app/assets/{network-Bts7VO94.js → network-DB88FZ2T.js} +1 -1
- package/static/app/assets/{primitives-BuSMEJY8.js → primitives-D8U9OaIo.js} +1 -1
- package/static/app/assets/search-B8jsVTw_.js +1 -0
- package/static/app/assets/{shield-alert-BfTO6X_G.js → shield-alert-DnsvO79P.js} +1 -1
- package/static/app/assets/{textarea-BjctW1oh.js → textarea-4RchwbTa.js} +1 -1
- package/static/app/assets/{useFocusTrap-CE43-LrA.js → useFocusTrap-CFJjy7cS.js} +1 -1
- package/static/app/assets/{useQuery-Bv3ejLL5.js → useQuery-3Reu-A4a.js} +1 -1
- package/static/app/assets/{users-CsNqLZAj.js → users-BTxYksxa.js} +1 -1
- package/static/app/assets/{utils-KFFdVG_t.js → utils-D02Vf-fw.js} +1 -1
- package/static/app/assets/{workspace-wdCvdyPF.js → workspace-CSJNgX-C.js} +1 -1
- package/static/app/index.html +4 -4
- package/static/sw.js +1 -1
- package/static/app/assets/Library-CBia2Fvm.js +0 -1
- package/static/app/assets/index-CQWdDU3z.css +0 -2
- package/static/app/assets/search-DZzxhWaQ.js +0 -1
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""Persisting one chat turn: redact, audit, store, then grow the Brain.
|
|
2
|
+
|
|
3
|
+
Extracted from the `app_factory._build` closure in 10.3.0. It was 66 lines
|
|
4
|
+
deep inside a 1,343-line composition root, which meant the one function that
|
|
5
|
+
decides what a chat message looks like *after* redaction — and what the audit
|
|
6
|
+
log records about it — could not be exercised without standing up the whole
|
|
7
|
+
application.
|
|
8
|
+
|
|
9
|
+
The order here is the contract, and it is deliberate:
|
|
10
|
+
|
|
11
|
+
1. **Redact first.** Everything downstream (the audit preview, the durable
|
|
12
|
+
store, the knowledge graph) sees the redacted text, never the original.
|
|
13
|
+
2. **Audit before storing.** The audit row carries a masked preview and the
|
|
14
|
+
sensitivity verdict, so a message that should not have been sent is
|
|
15
|
+
visible in the log even if the store write later fails.
|
|
16
|
+
3. **Ingest through the pipeline, not the store.** Chat messages enter the
|
|
17
|
+
Brain the same way files and web pages do, so they get provenance and the
|
|
18
|
+
hook lifecycle rather than a direct write that bypasses both.
|
|
19
|
+
4. **A failed ingest does not lose the message.** Graph growth is best-effort;
|
|
20
|
+
the conversation store is not.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import logging
|
|
26
|
+
from dataclasses import dataclass
|
|
27
|
+
from datetime import datetime
|
|
28
|
+
from typing import Any, Callable, Dict, Optional
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class HistoryWriterDeps:
|
|
35
|
+
"""Everything one chat turn needs, injected so this is testable in isolation."""
|
|
36
|
+
|
|
37
|
+
conversations: Any
|
|
38
|
+
append_audit_event: Callable[..., None]
|
|
39
|
+
classify_sensitive_message: Callable[[Dict[str, Any], int], Dict[str, Any]]
|
|
40
|
+
redact_secret_text: Callable[[str], str]
|
|
41
|
+
normalize_branding: Callable[[str], str]
|
|
42
|
+
ingestion_pipeline: Any = None
|
|
43
|
+
ingestion_item_factory: Any = None
|
|
44
|
+
enable_graph: bool = False
|
|
45
|
+
knowledge_graph: Any = None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def write_chat_turn(
|
|
49
|
+
role: str,
|
|
50
|
+
message: str,
|
|
51
|
+
*,
|
|
52
|
+
user_email: Optional[str] = None,
|
|
53
|
+
user_nickname: Optional[str] = None,
|
|
54
|
+
source: Optional[str] = None,
|
|
55
|
+
conversation_id: Optional[str] = None,
|
|
56
|
+
workspace_id: Optional[str] = None,
|
|
57
|
+
deps: HistoryWriterDeps,
|
|
58
|
+
) -> None:
|
|
59
|
+
"""Persist one chat turn. Never raises: a failure here must not lose a reply."""
|
|
60
|
+
redact_secret_text = deps.redact_secret_text
|
|
61
|
+
normalize_branding = deps.normalize_branding
|
|
62
|
+
classify_sensitive_message = deps.classify_sensitive_message
|
|
63
|
+
append_audit_event = deps.append_audit_event
|
|
64
|
+
conversations = deps.conversations
|
|
65
|
+
ingestion_pipeline = deps.ingestion_pipeline
|
|
66
|
+
ingestion_item_factory = deps.ingestion_item_factory
|
|
67
|
+
enable_graph = deps.enable_graph
|
|
68
|
+
knowledge_graph = deps.knowledge_graph
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
# 1. Redact before anything else sees the text.
|
|
72
|
+
message = redact_secret_text(message)
|
|
73
|
+
if role == "assistant":
|
|
74
|
+
message = normalize_branding(message)
|
|
75
|
+
|
|
76
|
+
item: Dict[str, Any] = {
|
|
77
|
+
"role": role,
|
|
78
|
+
"content": message,
|
|
79
|
+
"timestamp": datetime.now().isoformat(),
|
|
80
|
+
}
|
|
81
|
+
for key, value in (
|
|
82
|
+
("user_email", user_email),
|
|
83
|
+
("user_nickname", user_nickname),
|
|
84
|
+
("source", source),
|
|
85
|
+
("conversation_id", conversation_id),
|
|
86
|
+
("workspace_id", workspace_id),
|
|
87
|
+
):
|
|
88
|
+
if value:
|
|
89
|
+
item[key] = value
|
|
90
|
+
|
|
91
|
+
# 2. Audit carries a masked preview and the verdict, never the body.
|
|
92
|
+
sensitive = classify_sensitive_message(item, -1)
|
|
93
|
+
append_audit_event(
|
|
94
|
+
"chat_message",
|
|
95
|
+
role=role,
|
|
96
|
+
user_email=user_email,
|
|
97
|
+
user_nickname=user_nickname,
|
|
98
|
+
source=source,
|
|
99
|
+
conversation_id=conversation_id,
|
|
100
|
+
workspace_id=workspace_id,
|
|
101
|
+
content_preview=sensitive.get("preview"),
|
|
102
|
+
content_chars=len(message or ""),
|
|
103
|
+
sensitivity=sensitive.get("sensitivity"),
|
|
104
|
+
sensitive_labels=sensitive.get("labels") or [],
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# 3. Durable episodic memory. Unbounded SQLite; the old 50-message
|
|
108
|
+
# chat_history.json cap is gone.
|
|
109
|
+
conversations.append(item)
|
|
110
|
+
|
|
111
|
+
# 4. Best-effort Brain growth through the unified pipeline.
|
|
112
|
+
if enable_graph and knowledge_graph and ingestion_pipeline and ingestion_item_factory:
|
|
113
|
+
try:
|
|
114
|
+
ingestion_pipeline.ingest(
|
|
115
|
+
ingestion_item_factory(
|
|
116
|
+
source_type="chat_message",
|
|
117
|
+
text=message,
|
|
118
|
+
owner=user_email,
|
|
119
|
+
workspace_id=workspace_id,
|
|
120
|
+
conversation_id=conversation_id,
|
|
121
|
+
metadata={
|
|
122
|
+
"role": role,
|
|
123
|
+
"user_nickname": user_nickname,
|
|
124
|
+
"source": source,
|
|
125
|
+
"raw": item,
|
|
126
|
+
},
|
|
127
|
+
),
|
|
128
|
+
user_email=user_email,
|
|
129
|
+
)
|
|
130
|
+
except Exception as graph_error: # noqa: BLE001
|
|
131
|
+
# The message is already stored; a graph failure must not
|
|
132
|
+
# look like a lost turn.
|
|
133
|
+
logger.warning("knowledge graph message ingest failed: %s", graph_error)
|
|
134
|
+
except Exception as exc: # noqa: BLE001
|
|
135
|
+
logger.warning("save_to_history failed: %s", exc)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
__all__ = ["HistoryWriterDeps", "write_chat_turn"]
|
|
@@ -5,7 +5,12 @@ from __future__ import annotations
|
|
|
5
5
|
from typing import Any, Callable, Dict, Optional
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
def build_review_run_now_runner(
|
|
8
|
+
def build_review_run_now_runner(
|
|
9
|
+
platform: Any,
|
|
10
|
+
# Injected as `fastapi.HTTPException`, which takes (status_code, detail) —
|
|
11
|
+
# not the bare `Exception` signature the narrower annotation implied.
|
|
12
|
+
http_exception: Callable[..., Exception],
|
|
13
|
+
) -> Callable[..., Any]:
|
|
9
14
|
"""Build the Review Center run-now runner used by the API router.
|
|
10
15
|
|
|
11
16
|
The runner preserves the public contract: "Run now" previews/regenerates the
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"""The assembly state the build phases share.
|
|
2
|
+
|
|
3
|
+
``app_factory._build`` was one 1,300-line function. That was not an accident:
|
|
4
|
+
its sections genuinely depend on each other, and several of them close over
|
|
5
|
+
names bound *further down* — ``save_to_history`` calls ``append_audit_event``,
|
|
6
|
+
which is built ~150 lines later. Inside a single function that works, because a
|
|
7
|
+
closure resolves a free variable when it is called, not when it is defined.
|
|
8
|
+
Any naive extraction breaks exactly there (10.3.0 hit this and documented it).
|
|
9
|
+
|
|
10
|
+
``RuntimeContext`` is what makes the extraction safe. It is a mutable object
|
|
11
|
+
that every phase writes into and reads from, so a closure created in phase 2
|
|
12
|
+
that calls ``ctx.append_audit_event`` still resolves it at call time — the same
|
|
13
|
+
late binding the single function had, now with the shared state named instead
|
|
14
|
+
of implicit.
|
|
15
|
+
|
|
16
|
+
Two properties keep it honest:
|
|
17
|
+
|
|
18
|
+
* **Attributes are declared, not defaulted.** Reading one before its phase has
|
|
19
|
+
run raises ``AttributeError`` naming the attribute, rather than silently
|
|
20
|
+
yielding ``None`` and failing somewhere unrelated.
|
|
21
|
+
* **The phase order is a contract.** ``tests/unit/test_runtime_context.py``
|
|
22
|
+
fixes which phase produces which attribute, so reordering the phases in a way
|
|
23
|
+
that breaks a dependency fails a test instead of production.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
from typing import Any, Dict, List, Set
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class RuntimeContext:
|
|
32
|
+
"""Mutable state carried across the ordered application build phases."""
|
|
33
|
+
|
|
34
|
+
# ── phase 1: platform ────────────────────────────────────────────────
|
|
35
|
+
mx: Any
|
|
36
|
+
|
|
37
|
+
# ── phase 2: configuration and paths ─────────────────────────────────
|
|
38
|
+
config_arg: Any
|
|
39
|
+
config_runtime: Any
|
|
40
|
+
security_runtime: Any
|
|
41
|
+
CONFIG: Any
|
|
42
|
+
APP_VERSION: str
|
|
43
|
+
APP_MODE: str
|
|
44
|
+
IS_PUBLIC_MODE: bool
|
|
45
|
+
DEFAULT_HOST: str
|
|
46
|
+
DEFAULT_PORT: int
|
|
47
|
+
ENABLE_TELEGRAM: bool
|
|
48
|
+
ENABLE_GRAPH: bool
|
|
49
|
+
AUTOLOAD_MODELS: bool
|
|
50
|
+
MODEL_IDLE_UNLOAD_SECONDS: Any
|
|
51
|
+
ALLOW_MODEL_DOWNLOADS: bool
|
|
52
|
+
MODEL_DOWNLOAD_TIMEOUT: Any
|
|
53
|
+
ALLOW_LOCAL_MODELS: bool
|
|
54
|
+
REQUIRE_AUTH: bool
|
|
55
|
+
ALLOW_PLAINTEXT_API_KEYS: bool
|
|
56
|
+
CORS_ALLOW_NETWORK: bool
|
|
57
|
+
CORS_EXTRA_ORIGINS: Any
|
|
58
|
+
PUBLIC_MODEL: str
|
|
59
|
+
LOCAL_MODEL: Any
|
|
60
|
+
LOCAL_DRAFT_MODEL: Any
|
|
61
|
+
OPEN_REGISTRATION: bool
|
|
62
|
+
SSO_DISCOVERY_URL: Any
|
|
63
|
+
SSO_CLIENT_ID: Any
|
|
64
|
+
SSO_CLIENT_SECRET: Any
|
|
65
|
+
SSO_REDIRECT_URI: Any
|
|
66
|
+
SSO_PROVIDER_NAME: Any
|
|
67
|
+
INVITE_CODE: Any
|
|
68
|
+
INVITE_COOKIE_SECRET: Any
|
|
69
|
+
INVITE_GATE_ENABLED: bool
|
|
70
|
+
SECURE_COOKIES: bool
|
|
71
|
+
BASE_DIR: Any
|
|
72
|
+
DATA_DIR: Any
|
|
73
|
+
STATIC_DIR: Any
|
|
74
|
+
USERS_FILE: Any
|
|
75
|
+
HISTORY_FILE: Any
|
|
76
|
+
VPC_FILE: Any
|
|
77
|
+
AUDIT_FILE: Any
|
|
78
|
+
SSO_FILE: Any
|
|
79
|
+
keyring: Any
|
|
80
|
+
|
|
81
|
+
# ── phase 3: identity, audit, history ────────────────────────────────
|
|
82
|
+
load_mcp_installs: Any
|
|
83
|
+
mcp_public_item: Any
|
|
84
|
+
recommend_mcps: Any
|
|
85
|
+
install_mcp: Any
|
|
86
|
+
_SESSION_TTL: Any
|
|
87
|
+
_session_store: Any
|
|
88
|
+
create_session: Any
|
|
89
|
+
get_session_email: Any
|
|
90
|
+
invalidate_session: Any
|
|
91
|
+
load_users: Any
|
|
92
|
+
save_users: Any
|
|
93
|
+
user_id_for_email: Any
|
|
94
|
+
verify_and_migrate_password: Any
|
|
95
|
+
redact_secret_text: Any
|
|
96
|
+
get_audit_log: Any
|
|
97
|
+
append_audit_event: Any
|
|
98
|
+
classify_sensitive_message: Any
|
|
99
|
+
build_sensitivity_report: Any
|
|
100
|
+
build_admin_audit_report: Any
|
|
101
|
+
get_user_role: Any
|
|
102
|
+
_extract_bearer_token: Any
|
|
103
|
+
get_current_user: Any
|
|
104
|
+
require_user: Any
|
|
105
|
+
require_admin: Any
|
|
106
|
+
public_user: Any
|
|
107
|
+
_RATE_LIMIT_ENABLED: bool
|
|
108
|
+
enforce_rate_limit: Any
|
|
109
|
+
_check_rate_limit: Any
|
|
110
|
+
_client_ip: Any
|
|
111
|
+
_bytes_match_extension: Any
|
|
112
|
+
_host_is_loopback: Any
|
|
113
|
+
get_history_user: Any
|
|
114
|
+
get_user_api_key: Any
|
|
115
|
+
set_user_api_key: Any
|
|
116
|
+
get_sso_settings: Any
|
|
117
|
+
public_sso_config: Any
|
|
118
|
+
save_sso_config: Any
|
|
119
|
+
_get_sso_discovery: Any
|
|
120
|
+
_sso_states: Any
|
|
121
|
+
_sso_env_defaults: Any
|
|
122
|
+
load_vpc_config: Any
|
|
123
|
+
save_vpc_config: Any
|
|
124
|
+
|
|
125
|
+
# ── phase 4: brain, persistence ──────────────────────────────────────
|
|
126
|
+
EMBEDDING_PROFILE: Any
|
|
127
|
+
EMBEDDER: Any
|
|
128
|
+
STORAGE_ENGINE: Any
|
|
129
|
+
brain_runtime: Any
|
|
130
|
+
KNOWLEDGE_GRAPH: Any
|
|
131
|
+
CONVERSATIONS: Any
|
|
132
|
+
save_to_history: Any
|
|
133
|
+
HOOKS_REGISTRY: Any
|
|
134
|
+
LOCAL_KG_WATCHER: Any
|
|
135
|
+
REALTIME_BUS: Any
|
|
136
|
+
WORKSPACE_OS: Any
|
|
137
|
+
WORKSPACE_SERVICE: Any
|
|
138
|
+
INVITATION_STORE: Any
|
|
139
|
+
PLUGIN_REGISTRY: Any
|
|
140
|
+
TEMPLATE_CATALOG: Any
|
|
141
|
+
AGENT_REGISTRY: Any
|
|
142
|
+
MEMORY_SERVICE: Any
|
|
143
|
+
BRAIN_INTELLIGENCE: Any
|
|
144
|
+
AUTOMATION_INTELLIGENCE: Any
|
|
145
|
+
INGESTION_PIPELINE: Any
|
|
146
|
+
DEVICE_IDENTITY: Any
|
|
147
|
+
KG_PORTABILITY: Any
|
|
148
|
+
FUNNEL_METRICS: Any
|
|
149
|
+
get_history: Any
|
|
150
|
+
conversation_title: Any
|
|
151
|
+
group_history_conversations: Any
|
|
152
|
+
get_conversation_messages: Any
|
|
153
|
+
clear_history: Any
|
|
154
|
+
clear_conversation: Any
|
|
155
|
+
_history_allowed_workspaces_for: Any
|
|
156
|
+
_history_include_legacy_global: Any
|
|
157
|
+
_require_graph: Any
|
|
158
|
+
_workspace_graph: Any
|
|
159
|
+
|
|
160
|
+
# ── phase 5: domain services ─────────────────────────────────────────
|
|
161
|
+
model_router: Any
|
|
162
|
+
gardener: Any
|
|
163
|
+
CHAT_SERVICE: Any
|
|
164
|
+
SEARCH_SERVICE: Any
|
|
165
|
+
BRAIN_MEMORY: Any
|
|
166
|
+
CONTEXT_ASSEMBLER: Any
|
|
167
|
+
ARTIFACT_LEDGER: Any
|
|
168
|
+
_scoped_hybrid_search: Any
|
|
169
|
+
CHAT_AGENT_RUNTIME: Any
|
|
170
|
+
on_chat_message: Any
|
|
171
|
+
_recent_chat_context: Any
|
|
172
|
+
_workspace_settings_payload: Any
|
|
173
|
+
_workspace_models_payload: Any
|
|
174
|
+
_embedding_info: Any
|
|
175
|
+
_allowed_workspaces_for: Any
|
|
176
|
+
app_context: Any
|
|
177
|
+
|
|
178
|
+
# ── phase 6: web app and foundation routers ──────────────────────────
|
|
179
|
+
_spawn: Any
|
|
180
|
+
lifespan: Any
|
|
181
|
+
app: Any
|
|
182
|
+
model_runtime_service: Any
|
|
183
|
+
STATIC_ROUTES: Any
|
|
184
|
+
ui_file_response: Any
|
|
185
|
+
local_sysinfo: Any
|
|
186
|
+
invite_authorized: Any
|
|
187
|
+
auth_router: Any
|
|
188
|
+
admin_router: Any
|
|
189
|
+
invitations_router: Any
|
|
190
|
+
security_router: Any
|
|
191
|
+
_graph_stats_safe: Any
|
|
192
|
+
_product_hardening_status: Any
|
|
193
|
+
_security_audit_events_safe: Any
|
|
194
|
+
_security_list_uploaded_files: Any
|
|
195
|
+
|
|
196
|
+
# ── phase 7: platform features ───────────────────────────────────────
|
|
197
|
+
_llm_generate_sync: Any
|
|
198
|
+
PLATFORM: Any
|
|
199
|
+
_automation_runtime: Any
|
|
200
|
+
REVIEW_QUEUE: Any
|
|
201
|
+
TRIGGER_SERVICE: Any
|
|
202
|
+
AGENT_RUNTIME: Any
|
|
203
|
+
RUN_EXECUTOR: Any
|
|
204
|
+
COMMAND_CENTER: Any
|
|
205
|
+
CHANGE_PROPOSALS: Any
|
|
206
|
+
EVIDENCE_ACTIONS_SERVICE: Any
|
|
207
|
+
VOICE_CAPTURE: Any
|
|
208
|
+
PROJECT_SESSIONS: Any
|
|
209
|
+
|
|
210
|
+
# ── phase 8: interaction routers ─────────────────────────────────────
|
|
211
|
+
model_runtime: Any
|
|
212
|
+
|
|
213
|
+
# ── legacy compatibility surface (phase 9 reads these) ───────────────
|
|
214
|
+
legacy: Dict[str, Any]
|
|
215
|
+
|
|
216
|
+
def __init__(self, config: Any = None) -> None:
|
|
217
|
+
self.config_arg = config
|
|
218
|
+
self.legacy = {}
|
|
219
|
+
self._produced: Dict[str, str] = {}
|
|
220
|
+
self._phase: str = "<none>"
|
|
221
|
+
|
|
222
|
+
# ── phase bookkeeping ────────────────────────────────────────────────
|
|
223
|
+
def enter(self, phase: str) -> None:
|
|
224
|
+
"""Mark which phase subsequent ``set`` calls belong to."""
|
|
225
|
+
self._phase = phase
|
|
226
|
+
|
|
227
|
+
def set(self, **values: Any) -> None:
|
|
228
|
+
"""Publish values produced by the current phase.
|
|
229
|
+
|
|
230
|
+
Recording the producing phase is what lets the ordering test assert a
|
|
231
|
+
contract instead of a comment.
|
|
232
|
+
"""
|
|
233
|
+
for name, value in values.items():
|
|
234
|
+
setattr(self, name, value)
|
|
235
|
+
self._produced.setdefault(name, self._phase)
|
|
236
|
+
|
|
237
|
+
def adopt(self, stage: Any, *names: str) -> None:
|
|
238
|
+
"""Publish selected keys of a runtime-stage mapping onto the context."""
|
|
239
|
+
self.set(**{name: stage[name] for name in names})
|
|
240
|
+
|
|
241
|
+
def require(self, name: str) -> Any:
|
|
242
|
+
"""Read an attribute, failing with the phase contract if it is unset.
|
|
243
|
+
|
|
244
|
+
The bare ``AttributeError`` is already specific; this adds which phase
|
|
245
|
+
was supposed to have produced the value, which is the actual question
|
|
246
|
+
when a phase gets reordered.
|
|
247
|
+
"""
|
|
248
|
+
try:
|
|
249
|
+
return getattr(self, name)
|
|
250
|
+
except AttributeError:
|
|
251
|
+
producer = self._produced.get(name)
|
|
252
|
+
hint = f" (produced by phase {producer!r})" if producer else ""
|
|
253
|
+
raise RuntimeError(
|
|
254
|
+
f"RuntimeContext.{name} was read before it was built{hint}"
|
|
255
|
+
) from None
|
|
256
|
+
|
|
257
|
+
@property
|
|
258
|
+
def produced_by(self) -> Dict[str, str]:
|
|
259
|
+
"""Attribute name → the phase that published it, for the order test."""
|
|
260
|
+
return dict(self._produced)
|
|
261
|
+
|
|
262
|
+
def names(self) -> Set[str]:
|
|
263
|
+
return set(self._produced)
|
|
264
|
+
|
|
265
|
+
def phases_run(self) -> List[str]:
|
|
266
|
+
seen: List[str] = []
|
|
267
|
+
for phase in self._produced.values():
|
|
268
|
+
if phase not in seen:
|
|
269
|
+
seen.append(phase)
|
|
270
|
+
return seen
|
|
271
|
+
|
|
272
|
+
def __repr__(self) -> str: # pragma: no cover - debugging aid
|
|
273
|
+
return f"<RuntimeContext phase={self._phase!r} names={len(self._produced)}>"
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
__all__ = ["RuntimeContext"]
|
|
@@ -97,7 +97,7 @@ def build_sso_config_runtime(
|
|
|
97
97
|
|
|
98
98
|
async def _get_sso_discovery() -> Optional[Dict]:
|
|
99
99
|
settings = get_sso_settings()
|
|
100
|
-
url = settings.get("discovery_url", "")
|
|
100
|
+
url = str(settings.get("discovery_url", "") or "")
|
|
101
101
|
if _discovery_cache["data"] and _discovery_cache["url"] == url:
|
|
102
102
|
return _discovery_cache["data"]
|
|
103
103
|
if not url:
|
|
@@ -2,13 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from dataclasses import fields
|
|
6
|
-
from typing import Any, Iterator, Mapping
|
|
5
|
+
from dataclasses import Field, fields
|
|
6
|
+
from typing import TYPE_CHECKING, Any, ClassVar, Dict, Iterator, Mapping
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class RuntimeStage(Mapping[str, Any]):
|
|
10
10
|
"""Dataclass mixin that preserves the legacy mapping access during DI migration."""
|
|
11
11
|
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
# The mixin is only ever combined with @dataclass classes; declaring
|
|
14
|
+
# the marker attribute is how a type checker learns that `fields(self)`
|
|
15
|
+
# is legal here (see the dataclasses stubs' DataclassInstance protocol).
|
|
16
|
+
__dataclass_fields__: ClassVar[Dict[str, Field[Any]]]
|
|
17
|
+
|
|
12
18
|
def __getitem__(self, key: str) -> Any:
|
|
13
19
|
if key not in self:
|
|
14
20
|
raise KeyError(key)
|
|
@@ -97,3 +97,26 @@ class AppContext:
|
|
|
97
97
|
# persisted; ``None`` means no external chat mirror is registered. The
|
|
98
98
|
# telegram bridge subscribes here only when ENABLE_TELEGRAM is truthy.
|
|
99
99
|
on_chat_message: Optional[Callable[..., None]] = None
|
|
100
|
+
|
|
101
|
+
def require(self, field: str) -> Any:
|
|
102
|
+
"""Return a dependency this router cannot operate without.
|
|
103
|
+
|
|
104
|
+
Every field defaults to ``None`` so a test can build a context holding
|
|
105
|
+
only what it exercises. A router that binds an absent dependency used
|
|
106
|
+
to discover it as ``TypeError: 'NoneType' object is not callable``
|
|
107
|
+
inside a request handler, far from the wiring mistake. Binding through
|
|
108
|
+
``require`` moves that to router construction with the field named.
|
|
109
|
+
|
|
110
|
+
It is also what makes the binding statically honest: the declared type
|
|
111
|
+
is ``Optional[...]``, so a type checker reports every call site as
|
|
112
|
+
``"None" not callable`` — 105 of them in the workspace router alone.
|
|
113
|
+
``require`` states the precondition once instead.
|
|
114
|
+
"""
|
|
115
|
+
if field not in self.__dataclass_fields__:
|
|
116
|
+
raise AttributeError(f"AppContext has no field {field!r}")
|
|
117
|
+
value = getattr(self, field)
|
|
118
|
+
if value is None:
|
|
119
|
+
raise RuntimeError(
|
|
120
|
+
f"AppContext.{field} is required by this router but was not provided"
|
|
121
|
+
)
|
|
122
|
+
return value
|
|
@@ -143,7 +143,7 @@ class BrainIntelligenceService:
|
|
|
143
143
|
stale_cutoff = now - timedelta(days=_STALE_DAYS)
|
|
144
144
|
dated = [(_parse_ts(n.get("updated_at")), n) for n in nodes]
|
|
145
145
|
known = [pair for pair in dated if pair[0] is not None]
|
|
146
|
-
stale = [n for ts, n in known if ts < stale_cutoff]
|
|
146
|
+
stale = [n for ts, n in known if ts is not None and ts < stale_cutoff]
|
|
147
147
|
fresh_ratio = 1.0 - (len(stale) / len(known)) if known else 0.0
|
|
148
148
|
dimensions["freshness"] = {
|
|
149
149
|
"status": "ok",
|
|
@@ -576,7 +576,9 @@ class BrainIntelligenceService:
|
|
|
576
576
|
for c in conflicts
|
|
577
577
|
):
|
|
578
578
|
continue
|
|
579
|
-
other
|
|
579
|
+
other: Dict[str, Any] | None = next(
|
|
580
|
+
(r for r in memory_rows if r["id"] == other_id), None
|
|
581
|
+
)
|
|
580
582
|
conflicts.append({
|
|
581
583
|
"kind": "memory_pair",
|
|
582
584
|
"left_id": candidate.id,
|
|
@@ -46,7 +46,9 @@ class CloudTurnResult:
|
|
|
46
46
|
|
|
47
47
|
|
|
48
48
|
class CloudLLMAdapter(Protocol):
|
|
49
|
-
async def
|
|
49
|
+
# `def`, not `async def`: implementations are async generators, so the
|
|
50
|
+
# call itself returns the iterator rather than a coroutine wrapping it.
|
|
51
|
+
def stream(
|
|
50
52
|
self,
|
|
51
53
|
*,
|
|
52
54
|
system: str,
|
|
@@ -107,7 +109,7 @@ class CloudStreamingBridge:
|
|
|
107
109
|
answer_text=answer,
|
|
108
110
|
sent_node_ids=list(minimal.node_ids),
|
|
109
111
|
provider=getattr(self._adapter, "provider_name", "cloud"),
|
|
110
|
-
model=model or getattr(self._adapter, "default_model", ""),
|
|
112
|
+
model=str(model or getattr(self._adapter, "default_model", "")),
|
|
111
113
|
)
|
|
112
114
|
|
|
113
115
|
|
|
@@ -157,7 +159,7 @@ def plan_kg_expansion(result: CloudTurnResult) -> KGExpansionPlan:
|
|
|
157
159
|
)
|
|
158
160
|
|
|
159
161
|
return KGExpansionPlan(
|
|
160
|
-
conversation_title=conv_node["title"],
|
|
162
|
+
conversation_title=str(conv_node["title"]),
|
|
161
163
|
new_nodes=[conv_node],
|
|
162
164
|
new_edges=edges,
|
|
163
165
|
provenance={
|
|
@@ -185,7 +185,7 @@ class CommandCenterService:
|
|
|
185
185
|
"drafts": drafts,
|
|
186
186
|
}
|
|
187
187
|
if last_execution is not None:
|
|
188
|
-
section["last_execution"] = last_execution
|
|
188
|
+
section["last_execution"] = last_execution # type: ignore[assignment]
|
|
189
189
|
return section
|
|
190
190
|
|
|
191
191
|
def _review_section(
|
|
@@ -150,7 +150,8 @@ class EvidenceActionService:
|
|
|
150
150
|
missing.append(node_id)
|
|
151
151
|
continue
|
|
152
152
|
# /api/graph/node wraps the record; get_node returns it directly.
|
|
153
|
-
|
|
153
|
+
wrapped = node.get("node")
|
|
154
|
+
record: Mapping[str, Any] = wrapped if isinstance(wrapped, Mapping) else node
|
|
154
155
|
title = str(record.get("title") or record.get("id") or node_id)
|
|
155
156
|
body = str(record.get("summary") or record.get("content") or "").strip()
|
|
156
157
|
metadata = record.get("metadata")
|
|
@@ -202,7 +202,7 @@ async def stream_hybrid_cloud_turn(
|
|
|
202
202
|
answer_text=answer,
|
|
203
203
|
sent_node_ids=list(minimal.node_ids),
|
|
204
204
|
provider=getattr(chosen_adapter, "provider_name", "cloud"),
|
|
205
|
-
model=model or getattr(chosen_adapter, "default_model", ""),
|
|
205
|
+
model=str(model or getattr(chosen_adapter, "default_model", "")),
|
|
206
206
|
)
|
|
207
207
|
else:
|
|
208
208
|
result = await bridge.run_turn(
|
|
@@ -57,8 +57,9 @@ class LocalKnowledgeWatcher:
|
|
|
57
57
|
self._hooks = hooks
|
|
58
58
|
self._lock = threading.Lock()
|
|
59
59
|
self._watched: Dict[str, Dict[str, Any]] = {}
|
|
60
|
-
|
|
61
|
-
self.
|
|
60
|
+
# watchdog is optional; these stay None when it is not installed.
|
|
61
|
+
self._observer_cls: Optional[Any] = None
|
|
62
|
+
self._event_handler_base: Optional[Any] = None
|
|
62
63
|
self._import_error = ""
|
|
63
64
|
try:
|
|
64
65
|
from watchdog.events import FileSystemEventHandler
|
|
@@ -127,12 +128,21 @@ class LocalKnowledgeWatcher:
|
|
|
127
128
|
|
|
128
129
|
self.stop_source(source_id)
|
|
129
130
|
|
|
130
|
-
|
|
131
|
+
handler_base = self._event_handler_base
|
|
132
|
+
observer_cls = self._observer_cls
|
|
133
|
+
if handler_base is None or observer_cls is None:
|
|
134
|
+
return {
|
|
135
|
+
"watching": False,
|
|
136
|
+
"source_id": source_id,
|
|
137
|
+
"error": self._import_error or "watchdog is not installed",
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
class Handler(_LocalWatchHandler, handler_base): # type: ignore[misc, valid-type]
|
|
131
141
|
def __init__(handler_self):
|
|
132
|
-
|
|
142
|
+
handler_base.__init__(handler_self) # type: ignore[misc]
|
|
133
143
|
_LocalWatchHandler.__init__(handler_self, lambda: self._schedule(source_id))
|
|
134
144
|
|
|
135
|
-
observer =
|
|
145
|
+
observer = observer_cls()
|
|
136
146
|
try:
|
|
137
147
|
observer.schedule(Handler(), str(root), recursive=True)
|
|
138
148
|
observer.start()
|
|
@@ -161,6 +171,8 @@ class LocalKnowledgeWatcher:
|
|
|
161
171
|
timer.cancel()
|
|
162
172
|
observer = item.get("observer")
|
|
163
173
|
try:
|
|
174
|
+
if observer is None:
|
|
175
|
+
raise RuntimeError("watch entry has no observer")
|
|
164
176
|
observer.stop()
|
|
165
177
|
observer.join(timeout=3)
|
|
166
178
|
except Exception as exc:
|
|
@@ -285,8 +297,10 @@ def create_local_knowledge_router(
|
|
|
285
297
|
async def knowledge_graph_local_sources(request: Request):
|
|
286
298
|
require_user(request)
|
|
287
299
|
payload = graph().local_sources()
|
|
288
|
-
watch_status
|
|
289
|
-
|
|
300
|
+
watch_status: Dict[str, Any] = (
|
|
301
|
+
watcher.status() if watcher else {"available": False, "active": {}}
|
|
302
|
+
)
|
|
303
|
+
active: Dict[str, Any] = dict(watch_status.get("active") or {})
|
|
290
304
|
for source in payload.get("sources", []):
|
|
291
305
|
source["watch_active"] = source.get("id") in active
|
|
292
306
|
source["watch_status"] = active.get(source.get("id"))
|
|
@@ -305,8 +319,10 @@ def create_local_knowledge_router(
|
|
|
305
319
|
require_user(request)
|
|
306
320
|
kg = graph()
|
|
307
321
|
payload = kg.local_source_health(error_samples=error_samples)
|
|
308
|
-
watch_status
|
|
309
|
-
|
|
322
|
+
watch_status: Dict[str, Any] = (
|
|
323
|
+
watcher.status() if watcher else {"available": False, "active": {}}
|
|
324
|
+
)
|
|
325
|
+
active = dict(watch_status.get("active") or {})
|
|
310
326
|
for folder in payload.get("folders", []):
|
|
311
327
|
folder["watch_active"] = folder.get("id") in active
|
|
312
328
|
payload["watch"] = watch_status
|