ltcai 10.3.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 +37 -36
- package/docs/CHANGELOG.md +77 -0
- package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
- package/docs/DEVELOPMENT.md +1 -1
- package/docs/MYPY_BACKLOG.md +59 -92
- 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 +1 -1
- 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/contracts.py +2 -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 -1322
- 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/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 +1 -1
- 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 +36 -36
- package/static/app/assets/{Act-DrKIGZeJ.js → Act-BK__Y5VE.js} +1 -1
- package/static/app/assets/{AdminConsole-CEV1VefA.js → AdminConsole-ZrTyN8_F.js} +1 -1
- package/static/app/assets/{Brain-BZSXnwWU.js → Brain-CYHDXpPB.js} +1 -1
- package/static/app/assets/{BrainHome-CEBd86k6.js → BrainHome-bbJulb5g.js} +1 -1
- package/static/app/assets/{BrainSignals-O4s6R7av.js → BrainSignals-CesSPtUP.js} +1 -1
- package/static/app/assets/{Capture-AGbOxsg-.js → Capture-p5upXmz2.js} +1 -1
- package/static/app/assets/{CommandPalette-CeqBjCn2.js → CommandPalette-0zkyC0RY.js} +1 -1
- package/static/app/assets/{Library-CXnbNiQW.js → Library-Dt-vABI2.js} +1 -1
- package/static/app/assets/{LivingBrain-D9Iei2ZJ.js → LivingBrain-Bz-h6LBH.js} +1 -1
- package/static/app/assets/{ProductFlow-JlXiDfMM.js → ProductFlow-DAZ7DJCE.js} +1 -1
- package/static/app/assets/{ReviewCard-QQly2bTV.js → ReviewCard-Bv0WGbY6.js} +1 -1
- package/static/app/assets/{System-Bp9ZY4ER.js → System-BhrJQSD_.js} +1 -1
- package/static/app/assets/{activity-CxRPOQ2F.js → activity-AolpZJCH.js} +1 -1
- package/static/app/assets/{bot-1bcsWB2s.js → bot-CsVsT2nd.js} +1 -1
- package/static/app/assets/{brain-CxaeKCRM.js → brain-vuppco5y.js} +1 -1
- package/static/app/assets/{button-ZYFqGan7.js → button-1BP1PJZ3.js} +1 -1
- package/static/app/assets/{circle-pause-B5NOIqvk.js → circle-pause-5paeCOtq.js} +1 -1
- package/static/app/assets/{circle-play-BiJVFXf7.js → circle-play-By8mFYRV.js} +1 -1
- package/static/app/assets/{cpu-Vvq0OKTr.js → cpu-DvUQxdKe.js} +1 -1
- package/static/app/assets/{download-SEcXzkx1.js → download-CFqYG90a.js} +1 -1
- package/static/app/assets/{folder-open-Dw1ff_Qa.js → folder-open-CVSnp-Dp.js} +1 -1
- package/static/app/assets/{hard-drive-B00fR7_r.js → hard-drive-Ckr4Vd6-.js} +1 -1
- package/static/app/assets/{index-Dm9VXl1o.js → index--0zKF5Ow.js} +3 -3
- package/static/app/assets/{input-DtdWYQEa.js → input-D-JKquXU.js} +1 -1
- package/static/app/assets/{network-CpjBu0kI.js → network-DB88FZ2T.js} +1 -1
- package/static/app/assets/{primitives-CwsudBwc.js → primitives-D8U9OaIo.js} +1 -1
- package/static/app/assets/search-B8jsVTw_.js +1 -0
- package/static/app/assets/{shield-alert-CpjBc-gQ.js → shield-alert-DnsvO79P.js} +1 -1
- package/static/app/assets/{textarea-DUPyjHQs.js → textarea-4RchwbTa.js} +1 -1
- package/static/app/assets/{useFocusTrap-BWcMtCWH.js → useFocusTrap-CFJjy7cS.js} +1 -1
- package/static/app/assets/{useQuery-Dg5fPqTS.js → useQuery-3Reu-A4a.js} +1 -1
- package/static/app/assets/{users-BgcdTQGM.js → users-BTxYksxa.js} +1 -1
- package/static/app/assets/{utils-a3YXFtEO.js → utils-D02Vf-fw.js} +1 -1
- package/static/app/assets/{workspace-BMUEiXUm.js → workspace-CSJNgX-C.js} +1 -1
- package/static/app/index.html +3 -3
- package/static/sw.js +1 -1
- package/static/app/assets/search-CwEJWBEA.js +0 -1
|
@@ -0,0 +1,1417 @@
|
|
|
1
|
+
"""The ordered phases that build the Lattice AI application.
|
|
2
|
+
|
|
3
|
+
Each phase reads what earlier phases published on the :class:`RuntimeContext`
|
|
4
|
+
and publishes its own results. The order below *is* the dependency order and is
|
|
5
|
+
fixed by ``tests/unit/test_runtime_context.py``:
|
|
6
|
+
|
|
7
|
+
1. ``platform`` — MLX/GPU device selection (the only step that touches hardware)
|
|
8
|
+
2. ``config`` — configuration, security settings, paths, filesystem layout
|
|
9
|
+
3. ``identity`` — users, sessions, audit, access control, API keys, SSO/VPC config
|
|
10
|
+
4. ``brain`` — embedder, knowledge graph, conversations, hooks, persistence, history
|
|
11
|
+
5. ``domain`` — model router, garden, chat service (needed by the web phase)
|
|
12
|
+
6. ``web`` — lifespan, the FastAPI app, model runtime, static + foundation routers
|
|
13
|
+
7. ``services`` — retrieval/context, chat agent runtime, the typed AppContext
|
|
14
|
+
8. ``foundation_routes`` — mount the foundation routers now that AppContext exists
|
|
15
|
+
9. ``platform_features`` — workspace platform, automation, review, command centre
|
|
16
|
+
10. ``interaction`` — model/chat/search/tools routers and the brain tail routers
|
|
17
|
+
|
|
18
|
+
Every heavy import lives *inside* a phase, never at module scope: importing
|
|
19
|
+
this module must stay free of GPU init, singleton construction, and filesystem
|
|
20
|
+
writes (``tests/unit/test_app_factory.py`` enforces that).
|
|
21
|
+
|
|
22
|
+
Why closures still appear here: several handlers must resolve a dependency at
|
|
23
|
+
call time rather than at construction time, because the dependency is built by
|
|
24
|
+
a later phase. Those read through ``ctx``, which is exactly the late binding
|
|
25
|
+
the original single function got from Python's closure rules.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import logging
|
|
31
|
+
from typing import Any, Dict, List, Optional
|
|
32
|
+
|
|
33
|
+
from latticeai.core.quiet import quiet
|
|
34
|
+
from latticeai.runtime.runtime_context import RuntimeContext
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# ── phase 1: platform ────────────────────────────────────────────────────────
|
|
38
|
+
def phase_platform(ctx: RuntimeContext) -> None:
|
|
39
|
+
"""Select the MLX Metal device in the main thread.
|
|
40
|
+
|
|
41
|
+
Kept first and alone: it is the one step that talks to hardware, and it
|
|
42
|
+
must happen on the main thread before any worker touches MLX.
|
|
43
|
+
"""
|
|
44
|
+
ctx.enter("platform")
|
|
45
|
+
try:
|
|
46
|
+
import mlx.core as mx
|
|
47
|
+
|
|
48
|
+
mx.set_default_device(mx.gpu) # type: ignore[arg-type]
|
|
49
|
+
print("✅ MLX Metal context initialized in main thread.")
|
|
50
|
+
except Exception as exc:
|
|
51
|
+
print(f"⚠️ MLX Metal context unavailable: {exc}")
|
|
52
|
+
mx = None # type: ignore[assignment]
|
|
53
|
+
ctx.set(mx=mx)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# ── phase 2: configuration and paths ─────────────────────────────────────────
|
|
57
|
+
def phase_config(ctx: RuntimeContext) -> None:
|
|
58
|
+
"""Parse configuration once and lay out the data directory."""
|
|
59
|
+
ctx.enter("config")
|
|
60
|
+
|
|
61
|
+
from latticeai.core.security import host_is_loopback as _host_is_loopback_impl
|
|
62
|
+
from latticeai.core.workspace_os import WORKSPACE_OS_VERSION
|
|
63
|
+
from latticeai.runtime.config_runtime import build_config_runtime
|
|
64
|
+
from latticeai.runtime.security_runtime import build_security_runtime
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
import keyring
|
|
68
|
+
except Exception:
|
|
69
|
+
keyring = None # type: ignore[assignment]
|
|
70
|
+
|
|
71
|
+
config_runtime = build_config_runtime(ctx.config_arg)
|
|
72
|
+
ctx.set(config_runtime=config_runtime, keyring=keyring)
|
|
73
|
+
ctx.adopt(
|
|
74
|
+
config_runtime,
|
|
75
|
+
"CONFIG",
|
|
76
|
+
"APP_MODE",
|
|
77
|
+
"IS_PUBLIC_MODE",
|
|
78
|
+
"DEFAULT_HOST",
|
|
79
|
+
"DEFAULT_PORT",
|
|
80
|
+
"ENABLE_TELEGRAM",
|
|
81
|
+
"ENABLE_GRAPH",
|
|
82
|
+
"AUTOLOAD_MODELS",
|
|
83
|
+
"MODEL_IDLE_UNLOAD_SECONDS",
|
|
84
|
+
"ALLOW_MODEL_DOWNLOADS",
|
|
85
|
+
"MODEL_DOWNLOAD_TIMEOUT",
|
|
86
|
+
"ALLOW_LOCAL_MODELS",
|
|
87
|
+
"REQUIRE_AUTH",
|
|
88
|
+
"ALLOW_PLAINTEXT_API_KEYS",
|
|
89
|
+
"CORS_ALLOW_NETWORK",
|
|
90
|
+
"CORS_EXTRA_ORIGINS",
|
|
91
|
+
"PUBLIC_MODEL",
|
|
92
|
+
"LOCAL_MODEL",
|
|
93
|
+
"LOCAL_DRAFT_MODEL",
|
|
94
|
+
)
|
|
95
|
+
ctx.set(
|
|
96
|
+
APP_VERSION=WORKSPACE_OS_VERSION,
|
|
97
|
+
OPEN_REGISTRATION=ctx.CONFIG.open_registration,
|
|
98
|
+
_RATE_LIMIT_ENABLED=ctx.CONFIG.rate_limit_enabled,
|
|
99
|
+
# Forwarded headers (X-Forwarded-For / CF-Connecting-IP) are honoured
|
|
100
|
+
# for IP rate limiting only when the direct peer is a trusted proxy.
|
|
101
|
+
_host_is_loopback=lambda host: _host_is_loopback_impl(host),
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
security_runtime = build_security_runtime(ctx.CONFIG)
|
|
105
|
+
ctx.set(security_runtime=security_runtime)
|
|
106
|
+
ctx.adopt(
|
|
107
|
+
security_runtime,
|
|
108
|
+
"SSO_DISCOVERY_URL",
|
|
109
|
+
"SSO_CLIENT_ID",
|
|
110
|
+
"SSO_CLIENT_SECRET",
|
|
111
|
+
"SSO_REDIRECT_URI",
|
|
112
|
+
"SSO_PROVIDER_NAME",
|
|
113
|
+
"INVITE_CODE",
|
|
114
|
+
"INVITE_COOKIE_SECRET",
|
|
115
|
+
"INVITE_GATE_ENABLED",
|
|
116
|
+
"SECURE_COOKIES",
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
from pathlib import Path
|
|
120
|
+
|
|
121
|
+
base_dir = Path(__file__).resolve().parent.parent.parent
|
|
122
|
+
data_dir = ctx.CONFIG.data_dir
|
|
123
|
+
data_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
124
|
+
try:
|
|
125
|
+
data_dir.chmod(0o700)
|
|
126
|
+
except OSError:
|
|
127
|
+
quiet()
|
|
128
|
+
|
|
129
|
+
ctx.set(
|
|
130
|
+
BASE_DIR=base_dir,
|
|
131
|
+
DATA_DIR=data_dir,
|
|
132
|
+
STATIC_DIR=ctx.CONFIG.static_dir,
|
|
133
|
+
USERS_FILE=data_dir / "users.json",
|
|
134
|
+
HISTORY_FILE=data_dir / "chat_history.json",
|
|
135
|
+
VPC_FILE=data_dir / "vpc_config.json",
|
|
136
|
+
AUDIT_FILE=data_dir / "audit_log.json",
|
|
137
|
+
SSO_FILE=data_dir / "sso_config.json",
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# ── phase 3: identity, audit, access ─────────────────────────────────────────
|
|
142
|
+
def phase_identity(ctx: RuntimeContext) -> None:
|
|
143
|
+
"""Users, sessions, audit, access control, API keys, SSO/VPC config."""
|
|
144
|
+
ctx.enter("identity")
|
|
145
|
+
|
|
146
|
+
from fastapi import HTTPException, Request
|
|
147
|
+
|
|
148
|
+
from latticeai.core.mcp_registry import create_mcp_install_state
|
|
149
|
+
from latticeai.core.security import (
|
|
150
|
+
check_ip_rate_limit as _check_ip_rate_limit,
|
|
151
|
+
)
|
|
152
|
+
from latticeai.core.security import (
|
|
153
|
+
client_ip as _client_ip_impl,
|
|
154
|
+
)
|
|
155
|
+
from latticeai.core.security import (
|
|
156
|
+
enforce_rate_limit as _enforce_rate_limit,
|
|
157
|
+
)
|
|
158
|
+
from latticeai.core.security import hash_password, verify_password
|
|
159
|
+
from latticeai.core.security import (
|
|
160
|
+
redact_secret_text as _redact_secret_text,
|
|
161
|
+
)
|
|
162
|
+
from latticeai.core.users import (
|
|
163
|
+
ensure_user_identity,
|
|
164
|
+
load_users_file,
|
|
165
|
+
migrate_knowledge_graph_identity,
|
|
166
|
+
save_users_file,
|
|
167
|
+
)
|
|
168
|
+
from latticeai.core.users import user_id_for_email as _user_id_for_email
|
|
169
|
+
from latticeai.runtime.access_runtime import build_access_runtime
|
|
170
|
+
from latticeai.runtime.audit_runtime import build_audit_runtime
|
|
171
|
+
from latticeai.runtime.bootstrap import build_session_runtime
|
|
172
|
+
from latticeai.runtime.network_config_runtime import build_vpc_runtime
|
|
173
|
+
from latticeai.runtime.sso_config_runtime import build_sso_config_runtime
|
|
174
|
+
from latticeai.runtime.user_key_runtime import build_user_key_runtime
|
|
175
|
+
|
|
176
|
+
# MCP install state (extracted to mcp_registry during the server decomp).
|
|
177
|
+
mcp_state = create_mcp_install_state(ctx.DATA_DIR)
|
|
178
|
+
ctx.adopt(
|
|
179
|
+
mcp_state,
|
|
180
|
+
"load_mcp_installs",
|
|
181
|
+
"mcp_public_item",
|
|
182
|
+
"recommend_mcps",
|
|
183
|
+
"install_mcp",
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
def load_users() -> Dict[str, Any]:
|
|
187
|
+
users = load_users_file(ctx.USERS_FILE)
|
|
188
|
+
email_to_id = {
|
|
189
|
+
email: str(user["id"])
|
|
190
|
+
for email, user in users.items()
|
|
191
|
+
if isinstance(user, dict) and user.get("id")
|
|
192
|
+
}
|
|
193
|
+
try:
|
|
194
|
+
migrate_knowledge_graph_identity(
|
|
195
|
+
ctx.DATA_DIR / "knowledge_graph.sqlite", email_to_id
|
|
196
|
+
)
|
|
197
|
+
except Exception as exc:
|
|
198
|
+
logging.warning("knowledge graph identity migration skipped: %s", exc)
|
|
199
|
+
try:
|
|
200
|
+
# WORKSPACE_OS is built in the `brain` phase; resolving it here (at
|
|
201
|
+
# call time) is why this stays a closure over ctx.
|
|
202
|
+
ctx.WORKSPACE_OS.migrate_workspace_identities(email_to_id)
|
|
203
|
+
except Exception as exc:
|
|
204
|
+
logging.warning("workspace identity migration skipped: %s", exc)
|
|
205
|
+
return users
|
|
206
|
+
|
|
207
|
+
def save_users(users: Dict[str, Any]) -> None:
|
|
208
|
+
save_users_file(ctx.USERS_FILE, users)
|
|
209
|
+
|
|
210
|
+
def user_id_for_email(email: Optional[str]) -> Optional[str]:
|
|
211
|
+
return _user_id_for_email(load_users(), email)
|
|
212
|
+
|
|
213
|
+
def verify_and_migrate_password(
|
|
214
|
+
email: str, plain: str, stored: str, users: Dict
|
|
215
|
+
) -> bool:
|
|
216
|
+
"""평문 비밀번호를 투명하게 해시로 마이그레이션. 마이그레이션 발생 시 audit log 남김."""
|
|
217
|
+
if ":" in stored and len(stored) > 64:
|
|
218
|
+
return verify_password(plain, stored)
|
|
219
|
+
if plain == stored:
|
|
220
|
+
users[email]["password"] = hash_password(plain)
|
|
221
|
+
save_users(users)
|
|
222
|
+
try:
|
|
223
|
+
ctx.append_audit_event(
|
|
224
|
+
"password_migrated_from_plaintext", user_email=email
|
|
225
|
+
)
|
|
226
|
+
except Exception as exc:
|
|
227
|
+
logging.warning("audit log failed on password migration: %s", exc)
|
|
228
|
+
logging.info("Migrated plaintext password to scrypt hash for %s", email)
|
|
229
|
+
return True
|
|
230
|
+
return False
|
|
231
|
+
|
|
232
|
+
def redact_secret_text(text: str) -> str:
|
|
233
|
+
return _redact_secret_text(text)
|
|
234
|
+
|
|
235
|
+
def _check_rate_limit(
|
|
236
|
+
ip: str, action: str, max_calls: int, window_secs: float
|
|
237
|
+
) -> None:
|
|
238
|
+
_check_ip_rate_limit(ip, action, max_calls=max_calls, window_secs=window_secs)
|
|
239
|
+
|
|
240
|
+
def _client_ip(request: Request) -> str:
|
|
241
|
+
return _client_ip_impl(request)
|
|
242
|
+
|
|
243
|
+
def enforce_rate_limit(email: str, bucket_key: str) -> None:
|
|
244
|
+
_enforce_rate_limit(email, bucket_key, enabled=ctx._RATE_LIMIT_ENABLED)
|
|
245
|
+
|
|
246
|
+
ctx.set(
|
|
247
|
+
load_users=load_users,
|
|
248
|
+
save_users=save_users,
|
|
249
|
+
user_id_for_email=user_id_for_email,
|
|
250
|
+
verify_and_migrate_password=verify_and_migrate_password,
|
|
251
|
+
redact_secret_text=redact_secret_text,
|
|
252
|
+
_check_rate_limit=_check_rate_limit,
|
|
253
|
+
_client_ip=_client_ip,
|
|
254
|
+
enforce_rate_limit=enforce_rate_limit,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
# Session token lifecycle; user_id_for_email is the injected subject resolver.
|
|
258
|
+
session_runtime = build_session_runtime(user_id_resolver=user_id_for_email)
|
|
259
|
+
ctx.adopt(
|
|
260
|
+
session_runtime,
|
|
261
|
+
"_SESSION_TTL",
|
|
262
|
+
"_session_store",
|
|
263
|
+
"create_session",
|
|
264
|
+
"get_session_email",
|
|
265
|
+
"invalidate_session",
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
# Audit is built after redaction is available, because every event is
|
|
269
|
+
# redacted before it is written.
|
|
270
|
+
audit_runtime = build_audit_runtime(
|
|
271
|
+
audit_file=ctx.AUDIT_FILE,
|
|
272
|
+
logging=logging,
|
|
273
|
+
redact_fn=redact_secret_text,
|
|
274
|
+
)
|
|
275
|
+
ctx.adopt(audit_runtime, "get_audit_log", "append_audit_event")
|
|
276
|
+
|
|
277
|
+
access_runtime = build_access_runtime(
|
|
278
|
+
config=ctx.CONFIG,
|
|
279
|
+
require_auth=ctx.REQUIRE_AUTH,
|
|
280
|
+
http_exception=HTTPException,
|
|
281
|
+
request_type=Request,
|
|
282
|
+
load_users=load_users,
|
|
283
|
+
get_session_email=ctx.get_session_email,
|
|
284
|
+
user_id_for_email=_user_id_for_email,
|
|
285
|
+
)
|
|
286
|
+
ctx.adopt(
|
|
287
|
+
access_runtime,
|
|
288
|
+
"get_user_role",
|
|
289
|
+
"_extract_bearer_token",
|
|
290
|
+
"get_current_user",
|
|
291
|
+
"require_user",
|
|
292
|
+
"require_admin",
|
|
293
|
+
"public_user",
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
user_key_runtime = build_user_key_runtime(
|
|
297
|
+
load_users=load_users,
|
|
298
|
+
save_users=save_users,
|
|
299
|
+
ensure_user_identity=ensure_user_identity,
|
|
300
|
+
keyring=ctx.keyring,
|
|
301
|
+
allow_plaintext_api_keys=ctx.ALLOW_PLAINTEXT_API_KEYS,
|
|
302
|
+
logging=logging,
|
|
303
|
+
http_exception=HTTPException,
|
|
304
|
+
)
|
|
305
|
+
ctx.adopt(
|
|
306
|
+
user_key_runtime, "get_history_user", "get_user_api_key", "set_user_api_key"
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
# SSO config + OIDC discovery share one closure, so saving the config
|
|
310
|
+
# actually invalidates the discovery cache.
|
|
311
|
+
sso_runtime = build_sso_config_runtime(
|
|
312
|
+
sso_file=ctx.SSO_FILE,
|
|
313
|
+
discovery_url=ctx.SSO_DISCOVERY_URL,
|
|
314
|
+
client_id=ctx.SSO_CLIENT_ID,
|
|
315
|
+
client_secret=ctx.SSO_CLIENT_SECRET,
|
|
316
|
+
redirect_uri=ctx.SSO_REDIRECT_URI,
|
|
317
|
+
provider_name=ctx.SSO_PROVIDER_NAME,
|
|
318
|
+
logging=logging,
|
|
319
|
+
)
|
|
320
|
+
ctx.adopt(
|
|
321
|
+
sso_runtime,
|
|
322
|
+
"_sso_env_defaults",
|
|
323
|
+
"get_sso_settings",
|
|
324
|
+
"public_sso_config",
|
|
325
|
+
"save_sso_config",
|
|
326
|
+
"_get_sso_discovery",
|
|
327
|
+
"_sso_states",
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
vpc_runtime = build_vpc_runtime(vpc_file=ctx.VPC_FILE, logging=logging)
|
|
331
|
+
ctx.adopt(vpc_runtime, "load_vpc_config", "save_vpc_config")
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
# ── phase 4: brain and persistence ───────────────────────────────────────────
|
|
335
|
+
def phase_brain(ctx: RuntimeContext) -> None:
|
|
336
|
+
"""Embedder, knowledge graph, conversations, hooks, persistence, history."""
|
|
337
|
+
ctx.enter("brain")
|
|
338
|
+
|
|
339
|
+
import os
|
|
340
|
+
|
|
341
|
+
from fastapi import HTTPException
|
|
342
|
+
|
|
343
|
+
from lattice_brain.graph.schema import set_embed_dim
|
|
344
|
+
from lattice_brain.ingestion import IngestionItem
|
|
345
|
+
from lattice_brain.storage import storage_from_env
|
|
346
|
+
from latticeai.core.audit import (
|
|
347
|
+
build_admin_audit_report as _build_admin_audit_report,
|
|
348
|
+
)
|
|
349
|
+
from latticeai.core.audit import (
|
|
350
|
+
build_sensitivity_report as _build_sensitivity_report,
|
|
351
|
+
)
|
|
352
|
+
from latticeai.core.audit import (
|
|
353
|
+
classify_sensitive_message as _classify_sensitive_message,
|
|
354
|
+
)
|
|
355
|
+
from latticeai.core.embedding_providers import (
|
|
356
|
+
resolve_embedder,
|
|
357
|
+
resolve_embedding_profile,
|
|
358
|
+
)
|
|
359
|
+
from latticeai.core.security import (
|
|
360
|
+
bytes_match_extension as _bytes_match_extension_impl,
|
|
361
|
+
)
|
|
362
|
+
from latticeai.models.router import normalize_branding
|
|
363
|
+
from latticeai.runtime.brain_runtime import build_brain_runtime
|
|
364
|
+
from latticeai.runtime.history_runtime import build_history_query_runtime
|
|
365
|
+
from latticeai.runtime.history_writer import HistoryWriterDeps, write_chat_turn
|
|
366
|
+
from latticeai.runtime.hooks_runtime import build_hooks_runtime
|
|
367
|
+
from latticeai.runtime.persistence_runtime import build_persistence_runtime
|
|
368
|
+
|
|
369
|
+
# Resolve the configured embedding provider once. Degrades to the offline
|
|
370
|
+
# hash fallback when unavailable, recording requested-vs-active provider.
|
|
371
|
+
try:
|
|
372
|
+
embedding_profile = resolve_embedding_profile(ctx.CONFIG.embedding_profile)
|
|
373
|
+
except ValueError as exc:
|
|
374
|
+
logging.warning("Embedding profile ignored: %s", exc)
|
|
375
|
+
embedding_profile = {}
|
|
376
|
+
|
|
377
|
+
provider = ctx.CONFIG.embedding_provider
|
|
378
|
+
model = ctx.CONFIG.embedding_model or str(embedding_profile.get("model") or "")
|
|
379
|
+
dim = ctx.CONFIG.embedding_dim or int(embedding_profile.get("dimensions") or 0)
|
|
380
|
+
if ctx.CONFIG.embedding_profile and provider in {"", "hash", "local", "fallback"}:
|
|
381
|
+
provider = str(embedding_profile.get("provider") or provider)
|
|
382
|
+
|
|
383
|
+
embedder = resolve_embedder(
|
|
384
|
+
provider,
|
|
385
|
+
model=model,
|
|
386
|
+
base_url=ctx.CONFIG.embedding_base_url,
|
|
387
|
+
api_key=ctx.CONFIG.embedding_api_key,
|
|
388
|
+
dim=dim,
|
|
389
|
+
timeout=ctx.CONFIG.embedding_timeout,
|
|
390
|
+
extra={"target": ctx.CONFIG.embedding_custom_target},
|
|
391
|
+
probe=provider not in {"", "hash", "local", "fallback"},
|
|
392
|
+
)
|
|
393
|
+
set_embed_dim(int(getattr(embedder, "dim", None) or dim or 384))
|
|
394
|
+
if embedder.fell_back:
|
|
395
|
+
logging.warning(
|
|
396
|
+
"Embedding provider %s unavailable: %s", embedder.requested, embedder.detail
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
storage_engine = (
|
|
400
|
+
storage_from_env(os.environ, data_dir=ctx.DATA_DIR) if ctx.ENABLE_GRAPH else None
|
|
401
|
+
)
|
|
402
|
+
ctx.set(
|
|
403
|
+
EMBEDDING_PROFILE=embedding_profile,
|
|
404
|
+
EMBEDDER=embedder,
|
|
405
|
+
STORAGE_ENGINE=storage_engine,
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
brain_runtime = build_brain_runtime(
|
|
409
|
+
data_dir=ctx.DATA_DIR,
|
|
410
|
+
history_file=ctx.HISTORY_FILE,
|
|
411
|
+
enable_graph=ctx.ENABLE_GRAPH,
|
|
412
|
+
embedder=embedder,
|
|
413
|
+
storage_engine=storage_engine,
|
|
414
|
+
)
|
|
415
|
+
ctx.set(brain_runtime=brain_runtime)
|
|
416
|
+
# CONVERSATIONS is the v4 durable episodic store: unbounded history in the
|
|
417
|
+
# same SQLite file as the graph, so backup/restore covers it for free.
|
|
418
|
+
ctx.adopt(brain_runtime, "KNOWLEDGE_GRAPH", "CONVERSATIONS")
|
|
419
|
+
|
|
420
|
+
def save_to_history(
|
|
421
|
+
role: str,
|
|
422
|
+
message: str,
|
|
423
|
+
user_email: Optional[str] = None,
|
|
424
|
+
user_nickname: Optional[str] = None,
|
|
425
|
+
source: Optional[str] = None,
|
|
426
|
+
conversation_id: Optional[str] = None,
|
|
427
|
+
workspace_id: Optional[str] = None,
|
|
428
|
+
) -> None:
|
|
429
|
+
"""Persist one chat turn. Logic lives in runtime/history_writer.py.
|
|
430
|
+
|
|
431
|
+
The dependency bundle is built per call because INGESTION_PIPELINE is
|
|
432
|
+
published later in this same phase; resolving through ctx keeps the
|
|
433
|
+
ordering contract in one place instead of reordering the phase.
|
|
434
|
+
"""
|
|
435
|
+
write_chat_turn(
|
|
436
|
+
role,
|
|
437
|
+
message,
|
|
438
|
+
user_email=user_email,
|
|
439
|
+
user_nickname=user_nickname,
|
|
440
|
+
source=source,
|
|
441
|
+
conversation_id=conversation_id,
|
|
442
|
+
workspace_id=workspace_id,
|
|
443
|
+
deps=HistoryWriterDeps(
|
|
444
|
+
conversations=ctx.CONVERSATIONS,
|
|
445
|
+
append_audit_event=ctx.append_audit_event,
|
|
446
|
+
classify_sensitive_message=ctx.classify_sensitive_message,
|
|
447
|
+
redact_secret_text=ctx.redact_secret_text,
|
|
448
|
+
normalize_branding=normalize_branding,
|
|
449
|
+
ingestion_pipeline=ctx.INGESTION_PIPELINE,
|
|
450
|
+
ingestion_item_factory=IngestionItem,
|
|
451
|
+
enable_graph=ctx.ENABLE_GRAPH,
|
|
452
|
+
knowledge_graph=ctx.KNOWLEDGE_GRAPH,
|
|
453
|
+
),
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
def classify_sensitive_message(item: Dict, index: int) -> Dict:
|
|
457
|
+
return _classify_sensitive_message(item, index)
|
|
458
|
+
|
|
459
|
+
def build_sensitivity_report(history: List[Dict]) -> Dict:
|
|
460
|
+
return _build_sensitivity_report(history)
|
|
461
|
+
|
|
462
|
+
def build_admin_audit_report(
|
|
463
|
+
users: Dict, audit_events: Optional[List[Dict]] = None
|
|
464
|
+
) -> Dict:
|
|
465
|
+
graph_stats = None
|
|
466
|
+
try:
|
|
467
|
+
if ctx.ENABLE_GRAPH and ctx.KNOWLEDGE_GRAPH:
|
|
468
|
+
graph_stats = ctx.KNOWLEDGE_GRAPH.stats()
|
|
469
|
+
except Exception:
|
|
470
|
+
quiet()
|
|
471
|
+
return _build_admin_audit_report(
|
|
472
|
+
ctx.AUDIT_FILE,
|
|
473
|
+
users,
|
|
474
|
+
get_user_role=ctx.get_user_role,
|
|
475
|
+
graph_stats=graph_stats,
|
|
476
|
+
audit_events=audit_events,
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
def _bytes_match_extension(data: bytes, ext: str) -> bool:
|
|
480
|
+
return _bytes_match_extension_impl(data, ext)
|
|
481
|
+
|
|
482
|
+
def _require_graph() -> None:
|
|
483
|
+
if not ctx.ENABLE_GRAPH or ctx.KNOWLEDGE_GRAPH is None:
|
|
484
|
+
raise HTTPException(
|
|
485
|
+
status_code=404,
|
|
486
|
+
detail=(
|
|
487
|
+
"지식 그래프가 비활성화되어 있습니다. "
|
|
488
|
+
"LATTICEAI_ENABLE_GRAPH=true 설정 후 다시 시도해 주세요."
|
|
489
|
+
),
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
def _workspace_graph() -> Any:
|
|
493
|
+
return (
|
|
494
|
+
ctx.KNOWLEDGE_GRAPH
|
|
495
|
+
if (ctx.ENABLE_GRAPH and ctx.KNOWLEDGE_GRAPH)
|
|
496
|
+
else None
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
ctx.set(
|
|
500
|
+
save_to_history=save_to_history,
|
|
501
|
+
classify_sensitive_message=classify_sensitive_message,
|
|
502
|
+
build_sensitivity_report=build_sensitivity_report,
|
|
503
|
+
build_admin_audit_report=build_admin_audit_report,
|
|
504
|
+
_bytes_match_extension=_bytes_match_extension,
|
|
505
|
+
_require_graph=_require_graph,
|
|
506
|
+
_workspace_graph=_workspace_graph,
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
# The hooks registry is built ahead of the watcher so folder-watch
|
|
510
|
+
# reindexes can fire the pre_index/post_index lifecycle hooks.
|
|
511
|
+
hooks_runtime = build_hooks_runtime(
|
|
512
|
+
data_dir=ctx.DATA_DIR,
|
|
513
|
+
enable_graph=ctx.ENABLE_GRAPH,
|
|
514
|
+
knowledge_graph_getter=lambda: ctx.KNOWLEDGE_GRAPH,
|
|
515
|
+
)
|
|
516
|
+
ctx.adopt(hooks_runtime, "HOOKS_REGISTRY", "LOCAL_KG_WATCHER")
|
|
517
|
+
|
|
518
|
+
persistence_runtime = build_persistence_runtime(
|
|
519
|
+
data_dir=ctx.DATA_DIR,
|
|
520
|
+
base_dir=ctx.BASE_DIR,
|
|
521
|
+
enable_graph=ctx.ENABLE_GRAPH,
|
|
522
|
+
knowledge_graph=ctx.KNOWLEDGE_GRAPH,
|
|
523
|
+
hooks_registry=ctx.HOOKS_REGISTRY,
|
|
524
|
+
history_file=ctx.HISTORY_FILE,
|
|
525
|
+
conversations=ctx.CONVERSATIONS,
|
|
526
|
+
user_id_for_email=ctx.user_id_for_email,
|
|
527
|
+
audit=lambda action, detail, user: ctx.append_audit_event(
|
|
528
|
+
action, user_email=user, **detail
|
|
529
|
+
),
|
|
530
|
+
)
|
|
531
|
+
ctx.adopt(
|
|
532
|
+
persistence_runtime,
|
|
533
|
+
"REALTIME_BUS",
|
|
534
|
+
"WORKSPACE_OS",
|
|
535
|
+
"WORKSPACE_SERVICE",
|
|
536
|
+
"INVITATION_STORE",
|
|
537
|
+
"PLUGIN_REGISTRY",
|
|
538
|
+
"TEMPLATE_CATALOG",
|
|
539
|
+
"AGENT_REGISTRY",
|
|
540
|
+
"MEMORY_SERVICE",
|
|
541
|
+
"BRAIN_INTELLIGENCE",
|
|
542
|
+
"AUTOMATION_INTELLIGENCE",
|
|
543
|
+
"INGESTION_PIPELINE",
|
|
544
|
+
"DEVICE_IDENTITY",
|
|
545
|
+
"KG_PORTABILITY",
|
|
546
|
+
"FUNNEL_METRICS",
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
# History reads/clears. The write path (save_to_history above) stays a
|
|
550
|
+
# closure because it is bound to redaction/audit/ingestion.
|
|
551
|
+
history_query_runtime = build_history_query_runtime(
|
|
552
|
+
conversations=ctx.CONVERSATIONS,
|
|
553
|
+
workspace_service=ctx.WORKSPACE_SERVICE,
|
|
554
|
+
require_auth=ctx.REQUIRE_AUTH,
|
|
555
|
+
logging=logging,
|
|
556
|
+
)
|
|
557
|
+
ctx.adopt(
|
|
558
|
+
history_query_runtime,
|
|
559
|
+
"_history_allowed_workspaces_for",
|
|
560
|
+
"_history_include_legacy_global",
|
|
561
|
+
"get_history",
|
|
562
|
+
"conversation_title",
|
|
563
|
+
"group_history_conversations",
|
|
564
|
+
"get_conversation_messages",
|
|
565
|
+
"clear_history",
|
|
566
|
+
"clear_conversation",
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
# ── phase 5: domain singletons (must precede the web app) ────────────────────
|
|
571
|
+
def phase_domain(ctx: RuntimeContext) -> None:
|
|
572
|
+
"""The model router, the garden, and the chat service.
|
|
573
|
+
|
|
574
|
+
Separated from :func:`phase_services` because ``phase_web`` needs the model
|
|
575
|
+
router: the lifespan hooks, the static status routes, and the model runtime
|
|
576
|
+
service are all wired against it.
|
|
577
|
+
"""
|
|
578
|
+
ctx.enter("domain")
|
|
579
|
+
|
|
580
|
+
from lattice_brain.graph.runtime import set_llm_router
|
|
581
|
+
from latticeai.models.router import LLMRouter
|
|
582
|
+
from latticeai.services.chat_service import ChatService
|
|
583
|
+
from latticeai.services.p_reinforce import PReinforceGardener
|
|
584
|
+
from latticeai.services.tool_dispatch import configure_tool_dispatch
|
|
585
|
+
|
|
586
|
+
model_router = LLMRouter()
|
|
587
|
+
set_llm_router(model_router)
|
|
588
|
+
configure_tool_dispatch(load_users=ctx.load_users, get_user_role=ctx.get_user_role)
|
|
589
|
+
ctx.set(model_router=model_router)
|
|
590
|
+
|
|
591
|
+
# v4 garden absorption: the vault is the user-owned markdown mirror and the
|
|
592
|
+
# brain is authoritative. Existing notes import idempotently (content-hash
|
|
593
|
+
# dedup), and garden context queries the brain rather than rescanning.
|
|
594
|
+
gardener = PReinforceGardener(
|
|
595
|
+
ingestion_pipeline=ctx.INGESTION_PIPELINE if ctx.ENABLE_GRAPH else None,
|
|
596
|
+
knowledge_graph=ctx.KNOWLEDGE_GRAPH,
|
|
597
|
+
)
|
|
598
|
+
if ctx.ENABLE_GRAPH:
|
|
599
|
+
try:
|
|
600
|
+
garden_import = gardener.import_vault()
|
|
601
|
+
if garden_import.get("failed"):
|
|
602
|
+
logging.warning(
|
|
603
|
+
"garden vault import: %s notes failed to ingest",
|
|
604
|
+
garden_import["failed"],
|
|
605
|
+
)
|
|
606
|
+
except Exception as exc:
|
|
607
|
+
logging.warning("garden vault import skipped: %s", exc)
|
|
608
|
+
ctx.set(gardener=gardener)
|
|
609
|
+
|
|
610
|
+
# The chat service owns persistence and trace behaviour once its user-key
|
|
611
|
+
# dependencies exist.
|
|
612
|
+
ctx.set(
|
|
613
|
+
CHAT_SERVICE=ChatService(
|
|
614
|
+
store=ctx.WORKSPACE_OS,
|
|
615
|
+
get_history=ctx.get_history,
|
|
616
|
+
save_to_history=ctx.save_to_history,
|
|
617
|
+
get_history_user=ctx.get_history_user,
|
|
618
|
+
)
|
|
619
|
+
)
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
# ── phase 6b: retrieval, agent runtime, and the typed AppContext ─────────────
|
|
623
|
+
def phase_services(ctx: RuntimeContext) -> None:
|
|
624
|
+
"""Retrieval/context assembly, the chat agent runtime, and the AppContext.
|
|
625
|
+
|
|
626
|
+
Runs after ``phase_web`` because the AppContext carries handles the web
|
|
627
|
+
phase produces (``ui_file_response``, ``local_sysinfo``, ``graph_stats``)
|
|
628
|
+
and the telegram mirror needs the lifespan's ``_spawn``.
|
|
629
|
+
"""
|
|
630
|
+
ctx.enter("services")
|
|
631
|
+
|
|
632
|
+
from latticeai.api.chat import build_recent_chat_context
|
|
633
|
+
from latticeai.core.enterprise import capability_registry
|
|
634
|
+
from latticeai.core.mcp_registry import (
|
|
635
|
+
SKILLS_DIR,
|
|
636
|
+
_fetch_skills_marketplace,
|
|
637
|
+
install_skill,
|
|
638
|
+
)
|
|
639
|
+
from latticeai.core.workspace_os import remove_skill_directory
|
|
640
|
+
from latticeai.runtime.chat_wiring import (
|
|
641
|
+
build_chat_agent_runtime_from_context,
|
|
642
|
+
maybe_build_telegram_chat_mirror,
|
|
643
|
+
)
|
|
644
|
+
from latticeai.runtime.context_runtime import build_context_runtime
|
|
645
|
+
from latticeai.services.app_context import AppContext
|
|
646
|
+
from latticeai.services.tool_dispatch import build_agent_runtime
|
|
647
|
+
from latticeai.setup.wizard import get_recommendations, scan_environment
|
|
648
|
+
from latticeai.tools import execute_tool, knowledge_save
|
|
649
|
+
|
|
650
|
+
gardener = ctx.gardener
|
|
651
|
+
model_router = ctx.model_router
|
|
652
|
+
|
|
653
|
+
def _workspace_settings_payload() -> Dict:
|
|
654
|
+
return {
|
|
655
|
+
"mode": ctx.APP_MODE,
|
|
656
|
+
"host": ctx.DEFAULT_HOST,
|
|
657
|
+
"port": ctx.DEFAULT_PORT,
|
|
658
|
+
"require_auth": ctx.REQUIRE_AUTH,
|
|
659
|
+
"enable_graph": ctx.ENABLE_GRAPH,
|
|
660
|
+
"allow_local_models": ctx.ALLOW_LOCAL_MODELS,
|
|
661
|
+
"static_dir": str(ctx.STATIC_DIR),
|
|
662
|
+
"data_dir": str(ctx.DATA_DIR),
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
def _workspace_models_payload() -> Dict:
|
|
666
|
+
return {
|
|
667
|
+
"current_model": ctx.model_router.current_model_id,
|
|
668
|
+
"loaded_models": ctx.model_router.loaded_model_ids,
|
|
669
|
+
"public_model": ctx.PUBLIC_MODEL,
|
|
670
|
+
"local_model": ctx.LOCAL_MODEL,
|
|
671
|
+
"local_draft_model": ctx.LOCAL_DRAFT_MODEL,
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
def _embedding_info() -> dict:
|
|
675
|
+
from latticeai.core.embedding_providers import (
|
|
676
|
+
PROVIDER_TYPES,
|
|
677
|
+
embedding_provider_profiles,
|
|
678
|
+
)
|
|
679
|
+
|
|
680
|
+
info = ctx.EMBEDDER.as_dict()
|
|
681
|
+
info["available_providers"] = list(PROVIDER_TYPES)
|
|
682
|
+
info["profile"] = ctx.CONFIG.embedding_profile or ""
|
|
683
|
+
info["profiles"] = embedding_provider_profiles()
|
|
684
|
+
return info
|
|
685
|
+
|
|
686
|
+
def _allowed_workspaces_for(user: Any) -> Any:
|
|
687
|
+
# No-auth local mode is single-user: no scoping. With auth, reads are
|
|
688
|
+
# scoped to the caller's memberships (legacy-global rows need an
|
|
689
|
+
# explicit maintenance-only opt-in).
|
|
690
|
+
if not ctx.REQUIRE_AUTH or not user:
|
|
691
|
+
return None
|
|
692
|
+
return ctx.PLATFORM.allowed_scopes(user)
|
|
693
|
+
|
|
694
|
+
def _recent_chat_context(
|
|
695
|
+
limit: int = 10,
|
|
696
|
+
include_image_missing_replies: bool = True,
|
|
697
|
+
user_email: Optional[str] = None,
|
|
698
|
+
conversation_id: Optional[str] = None,
|
|
699
|
+
workspace_id: Optional[str] = None,
|
|
700
|
+
) -> str:
|
|
701
|
+
return build_recent_chat_context(
|
|
702
|
+
get_history=ctx.get_history,
|
|
703
|
+
limit=limit,
|
|
704
|
+
include_image_missing_replies=include_image_missing_replies,
|
|
705
|
+
user_email=user_email,
|
|
706
|
+
conversation_id=conversation_id,
|
|
707
|
+
workspace_id=workspace_id,
|
|
708
|
+
)
|
|
709
|
+
|
|
710
|
+
ctx.set(
|
|
711
|
+
_workspace_settings_payload=_workspace_settings_payload,
|
|
712
|
+
_workspace_models_payload=_workspace_models_payload,
|
|
713
|
+
_embedding_info=_embedding_info,
|
|
714
|
+
_allowed_workspaces_for=_allowed_workspaces_for,
|
|
715
|
+
_recent_chat_context=_recent_chat_context,
|
|
716
|
+
)
|
|
717
|
+
|
|
718
|
+
context_runtime = build_context_runtime(
|
|
719
|
+
graph_store=ctx._workspace_graph(),
|
|
720
|
+
ingestion_pipeline=ctx.INGESTION_PIPELINE,
|
|
721
|
+
memory_service=ctx.MEMORY_SERVICE,
|
|
722
|
+
gardener=gardener,
|
|
723
|
+
require_auth=ctx.REQUIRE_AUTH,
|
|
724
|
+
allowed_scopes_for_user=lambda user_email: ctx.PLATFORM.allowed_scopes(
|
|
725
|
+
user_email
|
|
726
|
+
),
|
|
727
|
+
)
|
|
728
|
+
ctx.adopt(
|
|
729
|
+
context_runtime,
|
|
730
|
+
"SEARCH_SERVICE",
|
|
731
|
+
"BRAIN_MEMORY",
|
|
732
|
+
"CONTEXT_ASSEMBLER",
|
|
733
|
+
"ARTIFACT_LEDGER",
|
|
734
|
+
"_scoped_hybrid_search",
|
|
735
|
+
)
|
|
736
|
+
|
|
737
|
+
# Telegram chat mirror: registered only when ENABLE_TELEGRAM is truthy, so
|
|
738
|
+
# latticeai.api.chat never imports the 45KB telegram module.
|
|
739
|
+
ctx.set(
|
|
740
|
+
on_chat_message=maybe_build_telegram_chat_mirror(
|
|
741
|
+
enable_telegram=ctx.ENABLE_TELEGRAM,
|
|
742
|
+
spawn=ctx._spawn,
|
|
743
|
+
)
|
|
744
|
+
)
|
|
745
|
+
|
|
746
|
+
ctx.set(
|
|
747
|
+
CHAT_AGENT_RUNTIME=build_chat_agent_runtime_from_context(
|
|
748
|
+
build_agent_runtime=build_agent_runtime,
|
|
749
|
+
model_router=model_router,
|
|
750
|
+
execute_tool=execute_tool,
|
|
751
|
+
recent_chat_context=_recent_chat_context,
|
|
752
|
+
clear_history=ctx.clear_history,
|
|
753
|
+
knowledge_save=knowledge_save,
|
|
754
|
+
audit=ctx.append_audit_event,
|
|
755
|
+
hooks=ctx.HOOKS_REGISTRY,
|
|
756
|
+
brain_memory=ctx.BRAIN_MEMORY,
|
|
757
|
+
)
|
|
758
|
+
)
|
|
759
|
+
|
|
760
|
+
# One typed context object replaces the historical 25-30-kwarg wiring.
|
|
761
|
+
ctx.set(
|
|
762
|
+
app_context=AppContext(
|
|
763
|
+
config=ctx.CONFIG,
|
|
764
|
+
data_dir=ctx.DATA_DIR,
|
|
765
|
+
static_dir=ctx.STATIC_DIR,
|
|
766
|
+
base_dir=ctx.BASE_DIR,
|
|
767
|
+
skills_dir=SKILLS_DIR,
|
|
768
|
+
model_router=model_router,
|
|
769
|
+
workspace_store=ctx.WORKSPACE_OS,
|
|
770
|
+
workspace_service=ctx.WORKSPACE_SERVICE,
|
|
771
|
+
knowledge_graph=ctx.KNOWLEDGE_GRAPH,
|
|
772
|
+
local_kg_watcher=ctx.LOCAL_KG_WATCHER,
|
|
773
|
+
chat_service=ctx.CHAT_SERVICE,
|
|
774
|
+
context_assembler=ctx.CONTEXT_ASSEMBLER,
|
|
775
|
+
artifact_ledger=ctx.ARTIFACT_LEDGER,
|
|
776
|
+
brain_memory=ctx.BRAIN_MEMORY,
|
|
777
|
+
ingestion_pipeline=ctx.INGESTION_PIPELINE if ctx.ENABLE_GRAPH else None,
|
|
778
|
+
chat_agent_runtime=ctx.CHAT_AGENT_RUNTIME,
|
|
779
|
+
gardener=gardener,
|
|
780
|
+
hooks=ctx.HOOKS_REGISTRY,
|
|
781
|
+
realtime_bus=ctx.REALTIME_BUS,
|
|
782
|
+
capability_registry=capability_registry,
|
|
783
|
+
require_user=ctx.require_user,
|
|
784
|
+
require_admin=ctx.require_admin,
|
|
785
|
+
get_current_user=ctx.get_current_user,
|
|
786
|
+
load_users=ctx.load_users,
|
|
787
|
+
get_user_role=ctx.get_user_role,
|
|
788
|
+
enforce_rate_limit=ctx.enforce_rate_limit,
|
|
789
|
+
allowed_workspaces_for=ctx._history_allowed_workspaces_for,
|
|
790
|
+
append_audit_event=ctx.append_audit_event,
|
|
791
|
+
get_audit_log=ctx.get_audit_log,
|
|
792
|
+
get_history=ctx.get_history,
|
|
793
|
+
get_history_user=ctx.get_history_user,
|
|
794
|
+
save_to_history=ctx.save_to_history,
|
|
795
|
+
clear_history=ctx.clear_history,
|
|
796
|
+
clear_conversation=ctx.clear_conversation,
|
|
797
|
+
group_history_conversations=ctx.group_history_conversations,
|
|
798
|
+
get_conversation_messages=ctx.get_conversation_messages,
|
|
799
|
+
conversation_title=ctx.conversation_title,
|
|
800
|
+
enable_graph=ctx.ENABLE_GRAPH,
|
|
801
|
+
require_graph=ctx._require_graph,
|
|
802
|
+
workspace_graph=ctx._workspace_graph,
|
|
803
|
+
graph_stats=ctx._graph_stats_safe,
|
|
804
|
+
workspace_models=_workspace_models_payload,
|
|
805
|
+
workspace_settings=_workspace_settings_payload,
|
|
806
|
+
scan_environment=scan_environment,
|
|
807
|
+
local_sysinfo=ctx.local_sysinfo,
|
|
808
|
+
get_recommendations=get_recommendations,
|
|
809
|
+
fetch_skills_marketplace=_fetch_skills_marketplace,
|
|
810
|
+
install_skill=install_skill,
|
|
811
|
+
remove_skill_directory=remove_skill_directory,
|
|
812
|
+
redact_secret_text=ctx.redact_secret_text,
|
|
813
|
+
ui_file_response=ctx.ui_file_response,
|
|
814
|
+
public_model=ctx.PUBLIC_MODEL,
|
|
815
|
+
local_model=ctx.LOCAL_MODEL or "",
|
|
816
|
+
on_chat_message=ctx.on_chat_message,
|
|
817
|
+
funnel_metrics=ctx.FUNNEL_METRICS,
|
|
818
|
+
)
|
|
819
|
+
)
|
|
820
|
+
ctx.app.state.context = ctx.app_context
|
|
821
|
+
|
|
822
|
+
|
|
823
|
+
# ── phase 6: web app and foundation routers ──────────────────────────────────
|
|
824
|
+
def phase_web(ctx: RuntimeContext) -> None:
|
|
825
|
+
"""Lifespan, the FastAPI app, model runtime service, foundation routers."""
|
|
826
|
+
ctx.enter("web")
|
|
827
|
+
|
|
828
|
+
from latticeai.api.admin import create_admin_router
|
|
829
|
+
from latticeai.api.auth import create_auth_router
|
|
830
|
+
from latticeai.api.invitations import create_invitations_router
|
|
831
|
+
from latticeai.api.security_dashboard import (
|
|
832
|
+
create_security_router as _create_security_router,
|
|
833
|
+
)
|
|
834
|
+
from latticeai.api.static_routes import create_static_routes_router
|
|
835
|
+
from latticeai.core.policy import policy_matrix
|
|
836
|
+
from latticeai.core.product_hardening import build_product_hardening_status
|
|
837
|
+
from latticeai.core.security import hash_password
|
|
838
|
+
from latticeai.core.users import ensure_user_identity
|
|
839
|
+
from latticeai.runtime.lifespan_runtime import build_lifespan_runtime
|
|
840
|
+
from latticeai.runtime.model_wiring import configure_model_runtime_from_context
|
|
841
|
+
from latticeai.runtime.router_registration import (
|
|
842
|
+
build_auth_admin_security_router_bundle,
|
|
843
|
+
build_static_routes_bundle,
|
|
844
|
+
)
|
|
845
|
+
from latticeai.runtime.web_runtime import build_web_runtime
|
|
846
|
+
from latticeai.services.model_runtime import (
|
|
847
|
+
LOCAL_SERVER_PROCESSES,
|
|
848
|
+
build_model_runtime,
|
|
849
|
+
)
|
|
850
|
+
from latticeai.tools import ensure_agent_root
|
|
851
|
+
|
|
852
|
+
lifespan_runtime = build_lifespan_runtime(
|
|
853
|
+
app_mode=ctx.APP_MODE,
|
|
854
|
+
enable_telegram=ctx.ENABLE_TELEGRAM,
|
|
855
|
+
autoload_models=ctx.AUTOLOAD_MODELS,
|
|
856
|
+
is_public_mode=ctx.IS_PUBLIC_MODE,
|
|
857
|
+
public_model=ctx.PUBLIC_MODEL,
|
|
858
|
+
allow_local_models=ctx.ALLOW_LOCAL_MODELS,
|
|
859
|
+
local_model=ctx.LOCAL_MODEL,
|
|
860
|
+
local_draft_model=ctx.LOCAL_DRAFT_MODEL,
|
|
861
|
+
model_idle_unload_seconds=ctx.MODEL_IDLE_UNLOAD_SECONDS,
|
|
862
|
+
model_router=ctx.model_router,
|
|
863
|
+
local_kg_watcher=ctx.LOCAL_KG_WATCHER,
|
|
864
|
+
local_server_processes=LOCAL_SERVER_PROCESSES,
|
|
865
|
+
logger=logging,
|
|
866
|
+
)
|
|
867
|
+
ctx.adopt(lifespan_runtime, "_spawn", "lifespan")
|
|
868
|
+
|
|
869
|
+
web_runtime = build_web_runtime(
|
|
870
|
+
app_mode=ctx.APP_MODE,
|
|
871
|
+
app_version=ctx.APP_VERSION,
|
|
872
|
+
lifespan=ctx.lifespan,
|
|
873
|
+
default_host=ctx.DEFAULT_HOST,
|
|
874
|
+
default_port=ctx.DEFAULT_PORT,
|
|
875
|
+
cors_extra_origins=ctx.CORS_EXTRA_ORIGINS,
|
|
876
|
+
cors_allow_network=ctx.CORS_ALLOW_NETWORK,
|
|
877
|
+
static_dir=ctx.STATIC_DIR,
|
|
878
|
+
)
|
|
879
|
+
ctx.adopt(web_runtime, "app")
|
|
880
|
+
ensure_agent_root()
|
|
881
|
+
|
|
882
|
+
ctx.set(
|
|
883
|
+
model_runtime_service=configure_model_runtime_from_context(
|
|
884
|
+
build_model_runtime=build_model_runtime,
|
|
885
|
+
router=ctx.model_router,
|
|
886
|
+
APP_MODE=ctx.APP_MODE,
|
|
887
|
+
DEFAULT_HOST=ctx.DEFAULT_HOST,
|
|
888
|
+
DEFAULT_PORT=ctx.DEFAULT_PORT,
|
|
889
|
+
DATA_DIR=ctx.DATA_DIR,
|
|
890
|
+
BASE_DIR=ctx.BASE_DIR,
|
|
891
|
+
ENABLE_TELEGRAM=ctx.ENABLE_TELEGRAM,
|
|
892
|
+
ENABLE_GRAPH=ctx.ENABLE_GRAPH,
|
|
893
|
+
AUTOLOAD_MODELS=ctx.AUTOLOAD_MODELS,
|
|
894
|
+
MODEL_IDLE_UNLOAD_SECONDS=ctx.MODEL_IDLE_UNLOAD_SECONDS,
|
|
895
|
+
ALLOW_MODEL_DOWNLOADS=ctx.ALLOW_MODEL_DOWNLOADS,
|
|
896
|
+
MODEL_DOWNLOAD_TIMEOUT=ctx.MODEL_DOWNLOAD_TIMEOUT,
|
|
897
|
+
ALLOW_LOCAL_MODELS=ctx.ALLOW_LOCAL_MODELS,
|
|
898
|
+
REQUIRE_AUTH=ctx.REQUIRE_AUTH,
|
|
899
|
+
INVITE_GATE_ENABLED=ctx.INVITE_GATE_ENABLED,
|
|
900
|
+
ALLOW_PLAINTEXT_API_KEYS=ctx.ALLOW_PLAINTEXT_API_KEYS,
|
|
901
|
+
CORS_ALLOW_NETWORK=ctx.CORS_ALLOW_NETWORK,
|
|
902
|
+
PUBLIC_MODEL=ctx.PUBLIC_MODEL,
|
|
903
|
+
LOCAL_MODEL=ctx.LOCAL_MODEL,
|
|
904
|
+
IS_PUBLIC_MODE=ctx.IS_PUBLIC_MODE,
|
|
905
|
+
keyring=ctx.keyring,
|
|
906
|
+
get_current_user=ctx.get_current_user,
|
|
907
|
+
get_user_api_key=ctx.get_user_api_key,
|
|
908
|
+
)
|
|
909
|
+
)
|
|
910
|
+
|
|
911
|
+
static_routes_bundle = build_static_routes_bundle(
|
|
912
|
+
create_static_routes_router=create_static_routes_router,
|
|
913
|
+
static_dir=ctx.STATIC_DIR,
|
|
914
|
+
invite_gate_enabled=ctx.INVITE_GATE_ENABLED,
|
|
915
|
+
invite_code=ctx.INVITE_CODE,
|
|
916
|
+
invite_cookie_secret=ctx.INVITE_COOKIE_SECRET,
|
|
917
|
+
secure_cookies=ctx.SECURE_COOKIES,
|
|
918
|
+
app_mode=ctx.APP_MODE,
|
|
919
|
+
model_router=ctx.model_router,
|
|
920
|
+
require_user=ctx.require_user,
|
|
921
|
+
)
|
|
922
|
+
ctx.adopt(
|
|
923
|
+
static_routes_bundle,
|
|
924
|
+
"STATIC_ROUTES",
|
|
925
|
+
"ui_file_response",
|
|
926
|
+
"local_sysinfo",
|
|
927
|
+
"invite_authorized",
|
|
928
|
+
)
|
|
929
|
+
|
|
930
|
+
foundation_router_bundle = build_auth_admin_security_router_bundle(
|
|
931
|
+
create_auth_router=create_auth_router,
|
|
932
|
+
load_users=ctx.load_users,
|
|
933
|
+
save_users=ctx.save_users,
|
|
934
|
+
hash_password=hash_password,
|
|
935
|
+
verify_and_migrate_password=ctx.verify_and_migrate_password,
|
|
936
|
+
create_session=ctx.create_session,
|
|
937
|
+
get_session_email=ctx.get_session_email,
|
|
938
|
+
invalidate_session=ctx.invalidate_session,
|
|
939
|
+
extract_bearer_token=ctx._extract_bearer_token,
|
|
940
|
+
get_user_role=ctx.get_user_role,
|
|
941
|
+
require_user=ctx.require_user,
|
|
942
|
+
check_ip_rate_limit=ctx._check_rate_limit,
|
|
943
|
+
client_ip=ctx._client_ip,
|
|
944
|
+
get_sso_settings=ctx.get_sso_settings,
|
|
945
|
+
get_sso_discovery=ctx._get_sso_discovery,
|
|
946
|
+
public_sso_config=ctx.public_sso_config,
|
|
947
|
+
open_registration=ctx.OPEN_REGISTRATION,
|
|
948
|
+
session_ttl=ctx._SESSION_TTL,
|
|
949
|
+
require_auth=ctx.REQUIRE_AUTH,
|
|
950
|
+
secure_cookies=ctx.SECURE_COOKIES,
|
|
951
|
+
invite_authorized=ctx.invite_authorized,
|
|
952
|
+
ensure_identity=ensure_user_identity,
|
|
953
|
+
create_admin_router=create_admin_router,
|
|
954
|
+
require_admin=ctx.require_admin,
|
|
955
|
+
get_history=ctx.get_history,
|
|
956
|
+
get_audit_log=ctx.get_audit_log,
|
|
957
|
+
audit_file=ctx.AUDIT_FILE,
|
|
958
|
+
public_user=ctx.public_user,
|
|
959
|
+
load_vpc_config=ctx.load_vpc_config,
|
|
960
|
+
save_vpc_config=ctx.save_vpc_config,
|
|
961
|
+
build_admin_audit_report=ctx.build_admin_audit_report,
|
|
962
|
+
build_sensitivity_report=ctx.build_sensitivity_report,
|
|
963
|
+
append_audit_event=ctx.append_audit_event,
|
|
964
|
+
save_sso_config=ctx.save_sso_config,
|
|
965
|
+
knowledge_graph=ctx.KNOWLEDGE_GRAPH,
|
|
966
|
+
enable_graph=ctx.ENABLE_GRAPH,
|
|
967
|
+
logger=logging,
|
|
968
|
+
invite_code=ctx.INVITE_CODE,
|
|
969
|
+
invite_gate_enabled=ctx.INVITE_GATE_ENABLED,
|
|
970
|
+
default_port=ctx.DEFAULT_PORT,
|
|
971
|
+
policy_matrix=policy_matrix,
|
|
972
|
+
build_product_hardening_status=build_product_hardening_status,
|
|
973
|
+
config=ctx.CONFIG,
|
|
974
|
+
kg_portability=ctx.KG_PORTABILITY,
|
|
975
|
+
device_identity=ctx.DEVICE_IDENTITY,
|
|
976
|
+
create_invitations_router=create_invitations_router,
|
|
977
|
+
invitation_store=ctx.INVITATION_STORE,
|
|
978
|
+
workspace_service=ctx.WORKSPACE_SERVICE,
|
|
979
|
+
user_id_for_email=ctx.user_id_for_email,
|
|
980
|
+
create_security_router=_create_security_router,
|
|
981
|
+
classify_sensitive_message=ctx.classify_sensitive_message,
|
|
982
|
+
)
|
|
983
|
+
ctx.adopt(
|
|
984
|
+
foundation_router_bundle,
|
|
985
|
+
"auth_router",
|
|
986
|
+
"admin_router",
|
|
987
|
+
"invitations_router",
|
|
988
|
+
"security_router",
|
|
989
|
+
"_graph_stats_safe",
|
|
990
|
+
"_product_hardening_status",
|
|
991
|
+
"_security_audit_events_safe",
|
|
992
|
+
"_security_list_uploaded_files",
|
|
993
|
+
)
|
|
994
|
+
|
|
995
|
+
|
|
996
|
+
def phase_foundation_routes(ctx: RuntimeContext) -> None:
|
|
997
|
+
"""Mount the foundation routers once the AppContext exists."""
|
|
998
|
+
ctx.enter("foundation_routes")
|
|
999
|
+
|
|
1000
|
+
from latticeai.api.workspace import create_workspace_router
|
|
1001
|
+
from latticeai.runtime.router_registration import register_foundation_routers
|
|
1002
|
+
|
|
1003
|
+
register_foundation_routers(
|
|
1004
|
+
ctx.app,
|
|
1005
|
+
static_router=ctx.STATIC_ROUTES.router,
|
|
1006
|
+
auth_router=ctx.auth_router,
|
|
1007
|
+
admin_router=ctx.admin_router,
|
|
1008
|
+
invitations_router=ctx.invitations_router,
|
|
1009
|
+
security_router=ctx.security_router,
|
|
1010
|
+
create_workspace_router=create_workspace_router,
|
|
1011
|
+
context=ctx.app_context,
|
|
1012
|
+
)
|
|
1013
|
+
|
|
1014
|
+
|
|
1015
|
+
# ── phase 7: platform features ───────────────────────────────────────────────
|
|
1016
|
+
def phase_platform_features(ctx: RuntimeContext) -> None:
|
|
1017
|
+
"""Workspace platform, automation, review queue, command centre, proposals."""
|
|
1018
|
+
ctx.enter("platform_features")
|
|
1019
|
+
|
|
1020
|
+
from latticeai.api.agents import create_agents_router
|
|
1021
|
+
from latticeai.api.automation_intelligence import (
|
|
1022
|
+
create_automation_intelligence_router,
|
|
1023
|
+
)
|
|
1024
|
+
from latticeai.api.change_proposals import create_change_proposals_router
|
|
1025
|
+
from latticeai.api.command_center import create_command_center_router
|
|
1026
|
+
from latticeai.api.evidence_actions import create_evidence_actions_router
|
|
1027
|
+
from latticeai.api.funnel_metrics import create_funnel_metrics_router
|
|
1028
|
+
from latticeai.api.marketplace import create_marketplace_router
|
|
1029
|
+
from latticeai.api.plugins import create_plugins_router
|
|
1030
|
+
from latticeai.api.project_sessions import create_project_sessions_router
|
|
1031
|
+
from latticeai.api.realtime import create_realtime_router
|
|
1032
|
+
from latticeai.api.voice_capture import create_voice_capture_router
|
|
1033
|
+
from latticeai.api.workflow_designer import create_workflow_designer_router
|
|
1034
|
+
from latticeai.api.workspace import _workspace_scope_from_request
|
|
1035
|
+
from latticeai.core.project_sessions import ProjectSessionStore
|
|
1036
|
+
from latticeai.runtime.hooks_runtime import (
|
|
1037
|
+
bind_builtin_hook_runners,
|
|
1038
|
+
bind_trigger_hook_runner,
|
|
1039
|
+
)
|
|
1040
|
+
from latticeai.runtime.platform_runtime_wiring import (
|
|
1041
|
+
build_platform_automation_runtime,
|
|
1042
|
+
)
|
|
1043
|
+
from latticeai.runtime.router_registration import (
|
|
1044
|
+
register_platform_feature_routers,
|
|
1045
|
+
)
|
|
1046
|
+
from latticeai.services.change_proposals import ChangeProposalService
|
|
1047
|
+
from latticeai.services.command_center import CommandCenterService
|
|
1048
|
+
from latticeai.services.evidence_actions import EvidenceActionService
|
|
1049
|
+
from latticeai.services.tool_dispatch import get_tool_permission
|
|
1050
|
+
from latticeai.services.voice_capture import VoiceCaptureService
|
|
1051
|
+
from latticeai.tools import resolve_workspace_path
|
|
1052
|
+
|
|
1053
|
+
# v2 Agentic Workspace Platform: cross-system wiring.
|
|
1054
|
+
platform_automation_runtime = build_platform_automation_runtime(
|
|
1055
|
+
model_router=ctx.model_router,
|
|
1056
|
+
workspace_store=ctx.WORKSPACE_OS,
|
|
1057
|
+
workspace_service=ctx.WORKSPACE_SERVICE,
|
|
1058
|
+
plugin_registry=ctx.PLUGIN_REGISTRY,
|
|
1059
|
+
get_current_user=ctx.get_current_user,
|
|
1060
|
+
workspace_graph=ctx._workspace_graph,
|
|
1061
|
+
workspace_scope_from_request=_workspace_scope_from_request,
|
|
1062
|
+
get_tool_permission=get_tool_permission,
|
|
1063
|
+
hooks=ctx.HOOKS_REGISTRY,
|
|
1064
|
+
agent_registry=ctx.AGENT_REGISTRY,
|
|
1065
|
+
data_dir=ctx.DATA_DIR,
|
|
1066
|
+
append_audit_event=ctx.append_audit_event,
|
|
1067
|
+
memory_service=ctx.MEMORY_SERVICE,
|
|
1068
|
+
tz_name=getattr(ctx.CONFIG, "timezone", None),
|
|
1069
|
+
)
|
|
1070
|
+
ctx.adopt(
|
|
1071
|
+
platform_automation_runtime,
|
|
1072
|
+
"_llm_generate_sync",
|
|
1073
|
+
"PLATFORM",
|
|
1074
|
+
"_automation_runtime",
|
|
1075
|
+
"REVIEW_QUEUE",
|
|
1076
|
+
"TRIGGER_SERVICE",
|
|
1077
|
+
"AGENT_RUNTIME",
|
|
1078
|
+
"RUN_EXECUTOR",
|
|
1079
|
+
)
|
|
1080
|
+
bind_trigger_hook_runner(
|
|
1081
|
+
registry=ctx.HOOKS_REGISTRY, trigger_service=ctx.TRIGGER_SERVICE
|
|
1082
|
+
)
|
|
1083
|
+
ctx.app.state.run_executor = ctx.RUN_EXECUTOR
|
|
1084
|
+
ctx.app.state.run_reconciliation = ctx.RUN_EXECUTOR.reconcile_startup()
|
|
1085
|
+
ctx.TRIGGER_SERVICE.start()
|
|
1086
|
+
|
|
1087
|
+
bind_builtin_hook_runners(
|
|
1088
|
+
registry=ctx.HOOKS_REGISTRY,
|
|
1089
|
+
append_audit_event=ctx.append_audit_event,
|
|
1090
|
+
get_tool_permission=get_tool_permission,
|
|
1091
|
+
classify_sensitive_message=ctx.classify_sensitive_message,
|
|
1092
|
+
)
|
|
1093
|
+
|
|
1094
|
+
register_platform_feature_routers(
|
|
1095
|
+
ctx.app,
|
|
1096
|
+
create_plugins_router=create_plugins_router,
|
|
1097
|
+
plugin_registry=ctx.PLUGIN_REGISTRY,
|
|
1098
|
+
require_user=ctx.require_user,
|
|
1099
|
+
require_admin=ctx.require_admin,
|
|
1100
|
+
append_audit_event=ctx.append_audit_event,
|
|
1101
|
+
platform=ctx.PLATFORM,
|
|
1102
|
+
ui_file_response=ctx.ui_file_response,
|
|
1103
|
+
static_dir=ctx.STATIC_DIR,
|
|
1104
|
+
create_workflow_designer_router=create_workflow_designer_router,
|
|
1105
|
+
store=ctx.WORKSPACE_OS,
|
|
1106
|
+
get_current_user=ctx.get_current_user,
|
|
1107
|
+
workspace_graph=ctx._workspace_graph,
|
|
1108
|
+
hooks=ctx.HOOKS_REGISTRY,
|
|
1109
|
+
run_executor=ctx.RUN_EXECUTOR,
|
|
1110
|
+
trigger_service=ctx.TRIGGER_SERVICE,
|
|
1111
|
+
create_agents_router=create_agents_router,
|
|
1112
|
+
agent_runtime=ctx.AGENT_RUNTIME,
|
|
1113
|
+
create_marketplace_router=create_marketplace_router,
|
|
1114
|
+
template_catalog=ctx.TEMPLATE_CATALOG,
|
|
1115
|
+
create_realtime_router=create_realtime_router,
|
|
1116
|
+
realtime_bus=ctx.REALTIME_BUS,
|
|
1117
|
+
)
|
|
1118
|
+
|
|
1119
|
+
ctx.app.include_router(
|
|
1120
|
+
create_automation_intelligence_router(
|
|
1121
|
+
service=ctx.AUTOMATION_INTELLIGENCE,
|
|
1122
|
+
store=ctx.WORKSPACE_OS,
|
|
1123
|
+
require_user=ctx.require_user,
|
|
1124
|
+
gate_read=ctx.PLATFORM.gate_read,
|
|
1125
|
+
gate_write=ctx.PLATFORM.gate_write,
|
|
1126
|
+
append_audit_event=ctx.append_audit_event,
|
|
1127
|
+
workspace_graph=ctx._workspace_graph,
|
|
1128
|
+
run_executor=ctx.RUN_EXECUTOR,
|
|
1129
|
+
review_queue=ctx.REVIEW_QUEUE,
|
|
1130
|
+
)
|
|
1131
|
+
)
|
|
1132
|
+
# UX funnel metrics (backlog #16): admin-only runtime counters.
|
|
1133
|
+
ctx.app.include_router(
|
|
1134
|
+
create_funnel_metrics_router(
|
|
1135
|
+
service=ctx.FUNNEL_METRICS,
|
|
1136
|
+
require_admin=ctx.require_admin,
|
|
1137
|
+
)
|
|
1138
|
+
)
|
|
1139
|
+
|
|
1140
|
+
ctx.set(
|
|
1141
|
+
COMMAND_CENTER=CommandCenterService(
|
|
1142
|
+
conversation_store=ctx.CONVERSATIONS,
|
|
1143
|
+
knowledge_graph=ctx.KNOWLEDGE_GRAPH,
|
|
1144
|
+
store=ctx.WORKSPACE_OS,
|
|
1145
|
+
search_service=ctx.SEARCH_SERVICE,
|
|
1146
|
+
brain_intelligence=ctx.BRAIN_INTELLIGENCE,
|
|
1147
|
+
automation_intelligence=ctx.AUTOMATION_INTELLIGENCE,
|
|
1148
|
+
review_queue=ctx.REVIEW_QUEUE,
|
|
1149
|
+
enable_graph=ctx.ENABLE_GRAPH,
|
|
1150
|
+
)
|
|
1151
|
+
)
|
|
1152
|
+
ctx.app.include_router(
|
|
1153
|
+
create_command_center_router(
|
|
1154
|
+
service=ctx.COMMAND_CENTER,
|
|
1155
|
+
require_user=ctx.require_user,
|
|
1156
|
+
gate_read=ctx.PLATFORM.gate_read,
|
|
1157
|
+
)
|
|
1158
|
+
)
|
|
1159
|
+
|
|
1160
|
+
ctx.set(
|
|
1161
|
+
CHANGE_PROPOSALS=ChangeProposalService(
|
|
1162
|
+
review_queue=ctx.REVIEW_QUEUE,
|
|
1163
|
+
resolve_path=resolve_workspace_path,
|
|
1164
|
+
audit=ctx.append_audit_event,
|
|
1165
|
+
)
|
|
1166
|
+
)
|
|
1167
|
+
# Proposal-first mutations: the agent loop consults the governor so
|
|
1168
|
+
# additive creates run with minimal friction while changes and deletions
|
|
1169
|
+
# of existing files are staged for review instead of applied.
|
|
1170
|
+
ctx.CHAT_AGENT_RUNTIME.deps.change_governor = ctx.CHANGE_PROPOSALS
|
|
1171
|
+
ctx.app.include_router(
|
|
1172
|
+
create_change_proposals_router(
|
|
1173
|
+
service=ctx.CHANGE_PROPOSALS,
|
|
1174
|
+
require_user=ctx.require_user,
|
|
1175
|
+
gate_read=ctx.PLATFORM.gate_read,
|
|
1176
|
+
gate_write=ctx.PLATFORM.gate_write,
|
|
1177
|
+
)
|
|
1178
|
+
)
|
|
1179
|
+
|
|
1180
|
+
# Evidence → action (v9.9.6): an answer's citations become ready-to-send,
|
|
1181
|
+
# evidence-scoped follow-ups. Deterministic composition only — execution
|
|
1182
|
+
# stays on the chat/file-generation path.
|
|
1183
|
+
ctx.set(
|
|
1184
|
+
EVIDENCE_ACTIONS_SERVICE=EvidenceActionService(
|
|
1185
|
+
node_reader=getattr(ctx.KNOWLEDGE_GRAPH, "get_node", None)
|
|
1186
|
+
)
|
|
1187
|
+
)
|
|
1188
|
+
ctx.app.include_router(
|
|
1189
|
+
create_evidence_actions_router(
|
|
1190
|
+
service=ctx.EVIDENCE_ACTIONS_SERVICE,
|
|
1191
|
+
require_user=ctx.require_user,
|
|
1192
|
+
allowed_workspaces_for=ctx._allowed_workspaces_for,
|
|
1193
|
+
)
|
|
1194
|
+
)
|
|
1195
|
+
|
|
1196
|
+
# Voice memo capture (v9.9.7): the shortest path from a thought to the
|
|
1197
|
+
# Brain. Transcription is an optional local port — absent, the memo is
|
|
1198
|
+
# still stored and the response says it is not searchable.
|
|
1199
|
+
ctx.set(
|
|
1200
|
+
VOICE_CAPTURE=VoiceCaptureService(
|
|
1201
|
+
pipeline=ctx.INGESTION_PIPELINE if ctx.ENABLE_GRAPH else None,
|
|
1202
|
+
transcriber=None,
|
|
1203
|
+
)
|
|
1204
|
+
)
|
|
1205
|
+
ctx.app.include_router(
|
|
1206
|
+
create_voice_capture_router(
|
|
1207
|
+
service=ctx.VOICE_CAPTURE,
|
|
1208
|
+
require_user=ctx.require_user,
|
|
1209
|
+
gate_write=ctx.PLATFORM.gate_write,
|
|
1210
|
+
append_audit_event=ctx.append_audit_event,
|
|
1211
|
+
)
|
|
1212
|
+
)
|
|
1213
|
+
|
|
1214
|
+
# Multi-turn project loop (v9.9.6): work spanning several runs keeps its
|
|
1215
|
+
# files, open TODOs, and last honest verification in one project session.
|
|
1216
|
+
ctx.set(PROJECT_SESSIONS=ProjectSessionStore(ctx.DATA_DIR / "project_sessions"))
|
|
1217
|
+
ctx.app.include_router(
|
|
1218
|
+
create_project_sessions_router(
|
|
1219
|
+
store=ctx.PROJECT_SESSIONS,
|
|
1220
|
+
require_user=ctx.require_user,
|
|
1221
|
+
gate_read=ctx.PLATFORM.gate_read,
|
|
1222
|
+
gate_write=ctx.PLATFORM.gate_write,
|
|
1223
|
+
)
|
|
1224
|
+
)
|
|
1225
|
+
|
|
1226
|
+
|
|
1227
|
+
# ── phase 8: interaction routers ─────────────────────────────────────────────
|
|
1228
|
+
def phase_interaction(ctx: RuntimeContext) -> None:
|
|
1229
|
+
"""Model, chat, search, tools, hooks, memory and brain tail routers."""
|
|
1230
|
+
ctx.enter("interaction")
|
|
1231
|
+
|
|
1232
|
+
from fastapi import HTTPException
|
|
1233
|
+
|
|
1234
|
+
from latticeai.api.agent_registry import create_agent_registry_router
|
|
1235
|
+
from latticeai.api.brain_intelligence import create_brain_intelligence_router
|
|
1236
|
+
from latticeai.api.browser import create_browser_router
|
|
1237
|
+
from latticeai.api.chat import create_chat_router
|
|
1238
|
+
from latticeai.api.garden import create_garden_router
|
|
1239
|
+
from latticeai.api.health import create_health_router
|
|
1240
|
+
from latticeai.api.hooks import create_hooks_router
|
|
1241
|
+
from latticeai.api.memory import create_memory_router
|
|
1242
|
+
from latticeai.api.models import create_models_router
|
|
1243
|
+
from latticeai.api.network import create_network_router
|
|
1244
|
+
from latticeai.api.portability import create_portability_router
|
|
1245
|
+
from latticeai.api.review_queue import create_review_queue_router
|
|
1246
|
+
from latticeai.api.search import create_search_router
|
|
1247
|
+
from latticeai.api.setup import create_setup_router
|
|
1248
|
+
from latticeai.api.tools import create_tools_router
|
|
1249
|
+
from latticeai.core.model_compat import (
|
|
1250
|
+
list_cached_profiles as _list_compat_profiles,
|
|
1251
|
+
)
|
|
1252
|
+
from latticeai.runtime.chat_wiring import build_interaction_contexts
|
|
1253
|
+
from latticeai.runtime.model_wiring import register_model_runtime_routers
|
|
1254
|
+
from latticeai.runtime.platform_services_runtime import build_brain_network
|
|
1255
|
+
from latticeai.runtime.review_wiring import build_review_run_now_runner
|
|
1256
|
+
from latticeai.runtime.router_registration import (
|
|
1257
|
+
register_health_and_model_routers,
|
|
1258
|
+
register_interaction_routers,
|
|
1259
|
+
register_review_and_brain_tail_routers,
|
|
1260
|
+
)
|
|
1261
|
+
from latticeai.services.model_runtime import (
|
|
1262
|
+
CLOUD_VERIFY_TTL_SECONDS,
|
|
1263
|
+
ENGINE_MODEL_CATALOG,
|
|
1264
|
+
MODEL_ENGINE_ALIASES,
|
|
1265
|
+
download_hf_model,
|
|
1266
|
+
ensure_ollama_server,
|
|
1267
|
+
filter_lower_family_versions,
|
|
1268
|
+
local_binary,
|
|
1269
|
+
normalize_local_model_request,
|
|
1270
|
+
sse_event,
|
|
1271
|
+
)
|
|
1272
|
+
|
|
1273
|
+
service = ctx.model_runtime_service
|
|
1274
|
+
ctx.set(
|
|
1275
|
+
model_runtime=register_model_runtime_routers(
|
|
1276
|
+
app=ctx.app,
|
|
1277
|
+
create_health_router=create_health_router,
|
|
1278
|
+
create_models_router=create_models_router,
|
|
1279
|
+
register_health_and_model_routers=register_health_and_model_routers,
|
|
1280
|
+
model_router=ctx.model_router,
|
|
1281
|
+
runtime_service=service,
|
|
1282
|
+
runtime_features=service.runtime_features,
|
|
1283
|
+
is_public_mode=ctx.IS_PUBLIC_MODE,
|
|
1284
|
+
engine_status=service.engine_status,
|
|
1285
|
+
get_current_user=ctx.get_current_user,
|
|
1286
|
+
require_auth=ctx.REQUIRE_AUTH,
|
|
1287
|
+
app_version=ctx.APP_VERSION,
|
|
1288
|
+
app_mode=ctx.APP_MODE,
|
|
1289
|
+
require_user=ctx.require_user,
|
|
1290
|
+
require_admin=ctx.require_admin,
|
|
1291
|
+
load_users=ctx.load_users,
|
|
1292
|
+
get_user_role=ctx.get_user_role,
|
|
1293
|
+
install_engine=service.install_engine,
|
|
1294
|
+
verify_cloud_models=service.verify_cloud_models,
|
|
1295
|
+
normalize_local_model_request=normalize_local_model_request,
|
|
1296
|
+
download_hf_model=download_hf_model,
|
|
1297
|
+
prepare_and_load_model=service.prepare_and_load_model,
|
|
1298
|
+
prepare_and_load_model_stream=service.prepare_and_load_model_stream,
|
|
1299
|
+
sse_event=sse_event,
|
|
1300
|
+
ensure_ollama_server=ensure_ollama_server,
|
|
1301
|
+
local_binary=local_binary,
|
|
1302
|
+
filter_lower_family_versions=filter_lower_family_versions,
|
|
1303
|
+
list_compat_profiles=_list_compat_profiles,
|
|
1304
|
+
set_user_api_key=ctx.set_user_api_key,
|
|
1305
|
+
engine_model_catalog=ENGINE_MODEL_CATALOG,
|
|
1306
|
+
model_engine_aliases=MODEL_ENGINE_ALIASES,
|
|
1307
|
+
cloud_verify_ttl_seconds=CLOUD_VERIFY_TTL_SECONDS,
|
|
1308
|
+
allow_local_models=ctx.ALLOW_LOCAL_MODELS,
|
|
1309
|
+
)
|
|
1310
|
+
)
|
|
1311
|
+
|
|
1312
|
+
_tool_router_context, interaction_router_context = build_interaction_contexts(
|
|
1313
|
+
config=ctx.CONFIG,
|
|
1314
|
+
ingestion_pipeline=ctx.INGESTION_PIPELINE,
|
|
1315
|
+
data_dir=ctx.DATA_DIR,
|
|
1316
|
+
static_dir=ctx.STATIC_DIR,
|
|
1317
|
+
model_router=ctx.model_router,
|
|
1318
|
+
require_user=ctx.require_user,
|
|
1319
|
+
require_admin=ctx.require_admin,
|
|
1320
|
+
get_current_user=ctx.get_current_user,
|
|
1321
|
+
clear_history=ctx.clear_history,
|
|
1322
|
+
append_audit_event=ctx.append_audit_event,
|
|
1323
|
+
enforce_rate_limit=ctx.enforce_rate_limit,
|
|
1324
|
+
bytes_match_extension=ctx._bytes_match_extension,
|
|
1325
|
+
classify_sensitive_message=ctx.classify_sensitive_message,
|
|
1326
|
+
save_to_history=ctx.save_to_history,
|
|
1327
|
+
enable_graph=ctx.ENABLE_GRAPH,
|
|
1328
|
+
knowledge_graph=ctx.KNOWLEDGE_GRAPH,
|
|
1329
|
+
require_graph=ctx._require_graph,
|
|
1330
|
+
local_kg_watcher=ctx.LOCAL_KG_WATCHER,
|
|
1331
|
+
load_mcp_installs=ctx.load_mcp_installs,
|
|
1332
|
+
recommend_mcps=ctx.recommend_mcps,
|
|
1333
|
+
install_mcp=ctx.install_mcp,
|
|
1334
|
+
mcp_public_item=ctx.mcp_public_item,
|
|
1335
|
+
hooks=ctx.HOOKS_REGISTRY,
|
|
1336
|
+
workspace_service=ctx.WORKSPACE_SERVICE,
|
|
1337
|
+
chat_context=ctx.app_context,
|
|
1338
|
+
search_service=ctx.SEARCH_SERVICE,
|
|
1339
|
+
allowed_workspaces_for=ctx._allowed_workspaces_for,
|
|
1340
|
+
embedding_info=ctx._embedding_info,
|
|
1341
|
+
agent_registry=ctx.AGENT_REGISTRY,
|
|
1342
|
+
memory_service=ctx.MEMORY_SERVICE,
|
|
1343
|
+
platform=ctx.PLATFORM,
|
|
1344
|
+
active_model_getter=lambda: ctx.model_router.current_model_id or "",
|
|
1345
|
+
brain_intelligence=ctx.BRAIN_INTELLIGENCE,
|
|
1346
|
+
)
|
|
1347
|
+
register_interaction_routers(
|
|
1348
|
+
ctx.app,
|
|
1349
|
+
interaction_context=interaction_router_context,
|
|
1350
|
+
create_chat_router=create_chat_router,
|
|
1351
|
+
create_search_router=create_search_router,
|
|
1352
|
+
create_tools_router=create_tools_router,
|
|
1353
|
+
create_hooks_router=create_hooks_router,
|
|
1354
|
+
create_agent_registry_router=create_agent_registry_router,
|
|
1355
|
+
create_memory_router=create_memory_router,
|
|
1356
|
+
create_brain_intelligence_router=create_brain_intelligence_router,
|
|
1357
|
+
)
|
|
1358
|
+
|
|
1359
|
+
register_review_and_brain_tail_routers(
|
|
1360
|
+
ctx.app,
|
|
1361
|
+
create_review_queue_router=create_review_queue_router,
|
|
1362
|
+
review_queue=ctx.REVIEW_QUEUE,
|
|
1363
|
+
require_user=ctx.require_user,
|
|
1364
|
+
gate_read=ctx.PLATFORM.gate_read,
|
|
1365
|
+
gate_write=ctx.PLATFORM.gate_write,
|
|
1366
|
+
run_review_item=build_review_run_now_runner(ctx.PLATFORM, HTTPException),
|
|
1367
|
+
append_audit_event=ctx.append_audit_event,
|
|
1368
|
+
# Approving a change_proposal from the Review Center applies the
|
|
1369
|
+
# staged content through the same service the agent governor uses.
|
|
1370
|
+
change_proposals=ctx.CHANGE_PROPOSALS,
|
|
1371
|
+
create_browser_router=create_browser_router,
|
|
1372
|
+
ingestion_pipeline=ctx.INGESTION_PIPELINE,
|
|
1373
|
+
workspace_service=ctx.WORKSPACE_SERVICE,
|
|
1374
|
+
create_portability_router=create_portability_router,
|
|
1375
|
+
kg_portability=ctx.KG_PORTABILITY,
|
|
1376
|
+
require_admin=ctx.require_admin,
|
|
1377
|
+
build_brain_network=build_brain_network,
|
|
1378
|
+
device_identity=ctx.DEVICE_IDENTITY,
|
|
1379
|
+
data_dir=ctx.DATA_DIR,
|
|
1380
|
+
create_network_router=create_network_router,
|
|
1381
|
+
create_garden_router=create_garden_router,
|
|
1382
|
+
gardener=ctx.gardener,
|
|
1383
|
+
create_setup_router=create_setup_router,
|
|
1384
|
+
model_router=ctx.model_router,
|
|
1385
|
+
knowledge_graph=ctx.KNOWLEDGE_GRAPH,
|
|
1386
|
+
)
|
|
1387
|
+
|
|
1388
|
+
|
|
1389
|
+
#: The build order. Exported so the ordering test reads the same list the
|
|
1390
|
+
#: factory runs, rather than a copy that can drift.
|
|
1391
|
+
BUILD_PHASES = (
|
|
1392
|
+
phase_platform,
|
|
1393
|
+
phase_config,
|
|
1394
|
+
phase_identity,
|
|
1395
|
+
phase_brain,
|
|
1396
|
+
phase_domain,
|
|
1397
|
+
phase_web,
|
|
1398
|
+
phase_services,
|
|
1399
|
+
phase_foundation_routes,
|
|
1400
|
+
phase_platform_features,
|
|
1401
|
+
phase_interaction,
|
|
1402
|
+
)
|
|
1403
|
+
|
|
1404
|
+
|
|
1405
|
+
__all__ = [
|
|
1406
|
+
"BUILD_PHASES",
|
|
1407
|
+
"phase_brain",
|
|
1408
|
+
"phase_domain",
|
|
1409
|
+
"phase_config",
|
|
1410
|
+
"phase_foundation_routes",
|
|
1411
|
+
"phase_identity",
|
|
1412
|
+
"phase_interaction",
|
|
1413
|
+
"phase_platform",
|
|
1414
|
+
"phase_platform_features",
|
|
1415
|
+
"phase_services",
|
|
1416
|
+
"phase_web",
|
|
1417
|
+
]
|