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
|
@@ -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
|
|
@@ -236,7 +236,7 @@ class MemoryService:
|
|
|
236
236
|
elif kg_index:
|
|
237
237
|
vector_total = kg_index.get("indexed") or kg_index.get("ready")
|
|
238
238
|
|
|
239
|
-
sources = [
|
|
239
|
+
sources: List[Dict[str, Any]] = [
|
|
240
240
|
{
|
|
241
241
|
"id": "workspace", "type": "workspace", "label": "Workspace Memory",
|
|
242
242
|
"count": len(ws_mem), "size_bytes": ws_bytes if ws_mem else 0,
|
|
@@ -272,7 +272,7 @@ class MemoryService:
|
|
|
272
272
|
"detail": "Local embedding vector index." if kg_index else "Vector index unavailable.",
|
|
273
273
|
},
|
|
274
274
|
]
|
|
275
|
-
total_items = sum((s["count"] or 0) for s in sources)
|
|
275
|
+
total_items = sum(int(s["count"] or 0) for s in sources)
|
|
276
276
|
total_bytes = ws_bytes + kg_bytes + conv_bytes
|
|
277
277
|
healthy = sum(1 for s in sources if s["health"] == "ok")
|
|
278
278
|
overall = "ok" if healthy >= 4 else "degraded" if healthy >= 1 else "unavailable"
|
|
@@ -747,7 +747,11 @@ class MemoryService:
|
|
|
747
747
|
graph_count = int(sources.get("graph", {}).get("count") or 0)
|
|
748
748
|
vector_count = int(sources.get("vector", {}).get("count") or 0)
|
|
749
749
|
query = recall_query.strip() or self._latest_recall_query(user_email=user_email, workspace_id=workspace_id)
|
|
750
|
-
recall
|
|
750
|
+
recall: Dict[str, Any] = (
|
|
751
|
+
self.recall(query, user_email=user_email, workspace_id=workspace_id, limit=limit)
|
|
752
|
+
if query
|
|
753
|
+
else {"query": "", "results": [], "count": 0, "source": "live"}
|
|
754
|
+
)
|
|
751
755
|
recall_items = [
|
|
752
756
|
{
|
|
753
757
|
"id": item.get("id"),
|
|
@@ -760,7 +764,7 @@ class MemoryService:
|
|
|
760
764
|
"matched_terms": item.get("matched_terms") or [],
|
|
761
765
|
"confidence": item.get("confidence") or "low",
|
|
762
766
|
}
|
|
763
|
-
for item in recall.get("results", [])[: max(1, min(limit, 8))]
|
|
767
|
+
for item in list(recall.get("results", []))[: max(1, min(limit, 8))]
|
|
764
768
|
]
|
|
765
769
|
durable_items = workspace_count + conversation_count + graph_count
|
|
766
770
|
# Capability and proof are deliberately separated. The brain is
|
|
@@ -13,7 +13,7 @@ from __future__ import annotations
|
|
|
13
13
|
|
|
14
14
|
import re
|
|
15
15
|
import sys
|
|
16
|
-
from typing import Dict, List, Optional
|
|
16
|
+
from typing import Any, Dict, List, Optional
|
|
17
17
|
|
|
18
18
|
from latticeai.services.model_capability_registry import (
|
|
19
19
|
LOCAL_MLX_MODELS as _LOCAL_MLX_MODELS,
|
|
@@ -34,7 +34,7 @@ from latticeai.services.model_capability_registry import (
|
|
|
34
34
|
get_verified_models as _get_verified_models,
|
|
35
35
|
)
|
|
36
36
|
|
|
37
|
-
ENGINE_INSTALLERS = {
|
|
37
|
+
ENGINE_INSTALLERS: Dict[str, Dict[str, Any]] = {
|
|
38
38
|
"local_mlx": {
|
|
39
39
|
"command": [sys.executable, "-m", "pip", "install", "--upgrade", "mlx-vlm>=0.6.3", "mlx-lm", "huggingface_hub[cli]"],
|
|
40
40
|
"label": "Install MLX runtime",
|
|
@@ -89,10 +89,10 @@ ENGINE_INSTALLERS = {
|
|
|
89
89
|
# user-facing ENGINE_MODEL_CATALOG below is then narrowed to the aggressive 5.2.0
|
|
90
90
|
# policy (latest family generations only, no text-only/legacy weights) and the
|
|
91
91
|
# engine-specific ids/sizes are normalised. See `_finalize_engine_catalog`.
|
|
92
|
-
_RAW_ENGINE_MODEL_CATALOG: Dict[str, List[Dict[str,
|
|
92
|
+
_RAW_ENGINE_MODEL_CATALOG: Dict[str, List[Dict[str, Any]]] = _build_engine_model_catalog()
|
|
93
93
|
# Filled in at module end once the blocklist, alias map and family-version filter
|
|
94
94
|
# are all defined; declared here so the public name exists for static readers.
|
|
95
|
-
ENGINE_MODEL_CATALOG: Dict[str, List[Dict[str,
|
|
95
|
+
ENGINE_MODEL_CATALOG: Dict[str, List[Dict[str, Any]]] = {}
|
|
96
96
|
|
|
97
97
|
# Historical aliases preserved (used by _recommended_with_engine_options and resolution).
|
|
98
98
|
# These can be enriched later from registry if needed; kept verbatim for safety.
|
|
@@ -173,7 +173,7 @@ def _version_tuple(raw: str) -> tuple[int, ...]:
|
|
|
173
173
|
return tuple(int(part) for part in raw.split(".") if part.isdigit())
|
|
174
174
|
|
|
175
175
|
|
|
176
|
-
def _model_family_version(model: Dict[str,
|
|
176
|
+
def _model_family_version(model: Dict[str, Any]) -> Optional[tuple[str, tuple[int, ...]]]:
|
|
177
177
|
text = " ".join(str(model.get(key) or "") for key in ("family", "name", "id"))
|
|
178
178
|
for family, pattern in _VERSIONED_MODEL_PATTERNS:
|
|
179
179
|
match = pattern.search(text)
|
|
@@ -184,9 +184,9 @@ def _model_family_version(model: Dict[str, object]) -> Optional[tuple[str, tuple
|
|
|
184
184
|
return None
|
|
185
185
|
|
|
186
186
|
|
|
187
|
-
def filter_lower_family_versions(models: List[Dict[str,
|
|
187
|
+
def filter_lower_family_versions(models: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
188
188
|
max_versions: Dict[str, tuple[int, ...]] = {}
|
|
189
|
-
detected: List[tuple[Dict[str,
|
|
189
|
+
detected: List[tuple[Dict[str, Any], Optional[tuple[str, tuple[int, ...]]]]] = []
|
|
190
190
|
for model in models:
|
|
191
191
|
version_info = _model_family_version(model)
|
|
192
192
|
detected.append((model, version_info))
|
|
@@ -214,12 +214,12 @@ _BLOCKED_CATALOG_FRAGMENTS = (
|
|
|
214
214
|
)
|
|
215
215
|
|
|
216
216
|
|
|
217
|
-
def _is_blocked_catalog_id(model: Dict[str,
|
|
217
|
+
def _is_blocked_catalog_id(model: Dict[str, Any]) -> bool:
|
|
218
218
|
ident = str(model.get("id") or "").lower()
|
|
219
219
|
return any(fragment in ident for fragment in _BLOCKED_CATALOG_FRAGMENTS)
|
|
220
220
|
|
|
221
221
|
|
|
222
|
-
def _normalize_engine_entry(engine: str, model: Dict[str,
|
|
222
|
+
def _normalize_engine_entry(engine: str, model: Dict[str, Any]) -> Dict[str, Any]:
|
|
223
223
|
"""Apply historical engine-specific id + size conventions to a raw entry.
|
|
224
224
|
|
|
225
225
|
* Non-MLX engines resolve to their canonical packaged id via
|
|
@@ -243,9 +243,9 @@ def _normalize_engine_entry(engine: str, model: Dict[str, object]) -> Dict[str,
|
|
|
243
243
|
|
|
244
244
|
|
|
245
245
|
def _finalize_engine_catalog(
|
|
246
|
-
raw: Dict[str, List[Dict[str,
|
|
247
|
-
) -> Dict[str, List[Dict[str,
|
|
248
|
-
final: Dict[str, List[Dict[str,
|
|
246
|
+
raw: Dict[str, List[Dict[str, Any]]],
|
|
247
|
+
) -> Dict[str, List[Dict[str, Any]]]:
|
|
248
|
+
final: Dict[str, List[Dict[str, Any]]] = {}
|
|
249
249
|
for engine, models in raw.items():
|
|
250
250
|
kept = [
|
|
251
251
|
_normalize_engine_entry(engine, m)
|