ltcai 6.2.0 → 6.3.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.
Files changed (50) hide show
  1. package/README.md +18 -15
  2. package/docs/CHANGELOG.md +42 -0
  3. package/frontend/src/components/onboarding/AnalysisScreen.tsx +127 -0
  4. package/frontend/src/components/onboarding/DownloadConsentPanel.tsx +29 -0
  5. package/frontend/src/components/onboarding/InstallScreen.tsx +208 -0
  6. package/frontend/src/components/onboarding/LanguageChooser.tsx +23 -0
  7. package/frontend/src/components/onboarding/LoginScreen.tsx +131 -0
  8. package/frontend/src/components/onboarding/ProductFlowScreens.tsx +13 -688
  9. package/frontend/src/components/onboarding/RecommendationScreen.tsx +59 -0
  10. package/frontend/src/components/onboarding/recommendationModel.ts +132 -0
  11. package/frontend/src/features/admin/AdminConsole.tsx +1 -1
  12. package/frontend/src/features/brain/BrainCarePanel.tsx +254 -0
  13. package/frontend/src/features/brain/BrainComposer.tsx +66 -0
  14. package/frontend/src/features/brain/BrainConversation.tsx +131 -0
  15. package/frontend/src/features/brain/BrainGraphLayer.tsx +132 -0
  16. package/frontend/src/features/brain/BrainHome.tsx +28 -795
  17. package/frontend/src/features/brain/BrainMemoryLayer.tsx +38 -0
  18. package/frontend/src/features/brain/BrainOverviewPanel.tsx +74 -0
  19. package/frontend/src/features/brain/BrainRelationshipLayer.tsx +43 -0
  20. package/frontend/src/features/brain/DepthEmergence.tsx +53 -0
  21. package/frontend/src/features/brain/types.ts +6 -6
  22. package/frontend/src/features/review/ReviewCard.tsx +3 -1
  23. package/frontend/src/features/review/ReviewInbox.tsx +11 -1
  24. package/frontend/src/i18n.ts +92 -0
  25. package/frontend/src/pages/Brain.tsx +63 -5
  26. package/frontend/src/pages/Capture.tsx +102 -13
  27. package/frontend/src/pages/Library.tsx +72 -6
  28. package/lattice_brain/__init__.py +1 -1
  29. package/lattice_brain/runtime/multi_agent.py +1 -1
  30. package/latticeai/__init__.py +1 -1
  31. package/latticeai/app_factory.py +31 -39
  32. package/latticeai/core/marketplace.py +1 -1
  33. package/latticeai/core/workspace_os.py +1 -1
  34. package/latticeai/runtime/chat_wiring.py +119 -0
  35. package/latticeai/runtime/model_wiring.py +40 -0
  36. package/latticeai/runtime/platform_runtime_wiring.py +2 -3
  37. package/latticeai/runtime/review_wiring.py +37 -0
  38. package/latticeai/runtime/tail_wiring.py +21 -0
  39. package/package.json +2 -2
  40. package/scripts/check_i18n_literals.mjs +52 -0
  41. package/src-tauri/Cargo.lock +1 -1
  42. package/src-tauri/Cargo.toml +1 -1
  43. package/src-tauri/tauri.conf.json +1 -1
  44. package/static/app/asset-manifest.json +5 -5
  45. package/static/app/assets/{index-D91Rz5--.js → index-D76dWuQk.js} +2 -2
  46. package/static/app/assets/index-D76dWuQk.js.map +1 -0
  47. package/static/app/assets/index-Div5vMlq.css +2 -0
  48. package/static/app/index.html +2 -2
  49. package/static/app/assets/index-B2-1Gm0q.css +0 -2
  50. package/static/app/assets/index-D91Rz5--.js.map +0 -1
@@ -1,6 +1,6 @@
1
1
  import * as React from "react";
2
2
  import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
3
- import { FolderPlus, Globe2, HardDrive, Upload } from "lucide-react";
3
+ import { AlertCircle, CheckCircle2, FolderPlus, Globe2, HardDrive, Loader2, RotateCcw, Upload } from "lucide-react";
4
4
  import { latticeApi } from "@/api/client";
5
5
  import { ActionButton, DataPanel, EntityList, OperationResult, StructuredView, Tabs } from "@/components/primitives";
6
6
  import { Button } from "@/components/ui/button";
@@ -41,10 +41,20 @@ export function CapturePage({ initialTab }: { initialTab?: string }) {
41
41
  function FilesPanel() {
42
42
  const qc = useQueryClient();
43
43
  const docs = useQuery({ queryKey: ["documents"], queryFn: () => latticeApi.documents(200) });
44
+ const [queue, setQueue] = React.useState<UploadQueueItem[]>([]);
44
45
  const upload = useMutation({
45
- mutationFn: (files: FileList) => Promise.all(Array.from(files).map((file) => latticeApi.uploadDocument(file))),
46
- onSuccess: () => qc.invalidateQueries({ queryKey: ["documents"] }),
46
+ mutationFn: (files: File[]) => uploadFiles(files, setQueue),
47
+ onSuccess: () => {
48
+ void qc.invalidateQueries({ queryKey: ["documents"] });
49
+ void qc.invalidateQueries({ queryKey: ["graphStats"] });
50
+ void qc.invalidateQueries({ queryKey: ["memoryManager"] });
51
+ },
47
52
  });
53
+ const beginUpload = React.useCallback((files: FileList | File[]) => {
54
+ const nextFiles = Array.from(files);
55
+ if (!nextFiles.length) return;
56
+ upload.mutate(nextFiles);
57
+ }, [upload]);
48
58
  return (
49
59
  <div className="grid gap-4 xl:grid-cols-[0.75fr_1.25fr]">
50
60
  <Card>
@@ -53,26 +63,105 @@ function FilesPanel() {
53
63
  <CardDescription>Choose files and Lattice will prepare them for search and memory.</CardDescription>
54
64
  </CardHeader>
55
65
  <CardContent>
56
- <label className="flex min-h-56 cursor-pointer flex-col items-center justify-center gap-3 rounded-lg border border-dashed border-border bg-muted/30 p-6 text-center transition hover:bg-muted/50">
66
+ <label
67
+ className="flex min-h-56 cursor-pointer flex-col items-center justify-center gap-3 rounded-lg border border-dashed border-border bg-muted/30 p-6 text-center transition hover:bg-muted/50"
68
+ onDragOver={(event) => event.preventDefault()}
69
+ onDrop={(event) => {
70
+ event.preventDefault();
71
+ beginUpload(event.dataTransfer.files);
72
+ }}
73
+ >
57
74
  <Upload className="h-7 w-7 text-primary" />
58
- <span className="text-lg font-semibold">Choose files</span>
59
- <span className="max-w-sm text-sm leading-6 text-muted-foreground">PDF, Office files, notes, markdown, text, and spreadsheets are all welcome.</span>
60
- <input type="file" multiple className="sr-only" onChange={(e) => e.target.files && upload.mutate(e.target.files)} />
75
+ <span className="text-lg font-semibold">Drop files or choose documents</span>
76
+ <span className="max-w-sm text-sm leading-6 text-muted-foreground">Each file is queued, parsed, written to Memory, and linked into the Brain graph with source metadata.</span>
77
+ <input type="file" multiple className="sr-only" onChange={(e) => e.target.files && beginUpload(e.target.files)} />
61
78
  </label>
62
- {upload.data ? (
63
- <div className="mt-3 space-y-2">
64
- {upload.data.map((item, index) => <OperationResult key={index} result={item} successLabel="Upload completed" />)}
65
- </div>
66
- ) : null}
79
+ <DocumentUploadQueue queue={queue} onRetry={(file) => beginUpload([file])} />
67
80
  </CardContent>
68
81
  </Card>
69
82
  <DataPanel title="Uploaded documents" result={docs.data}>
70
- {(data) => <EntityList items={(data as Record<string, unknown>).documents || data} titleKey="filename" metaKey="ingest_state" limit={12} />}
83
+ {(data) => (
84
+ <div className="space-y-3">
85
+ <EntityList items={(data as Record<string, unknown>).documents || data} titleKey="filename" metaKey="ingest_state" limit={12} />
86
+ <div className="rounded-md border border-border bg-background/55 p-3 text-sm text-muted-foreground">
87
+ Completed uploads appear here after they enter Memory. Graph links may continue building briefly after parsing finishes.
88
+ </div>
89
+ </div>
90
+ )}
71
91
  </DataPanel>
72
92
  </div>
73
93
  );
74
94
  }
75
95
 
96
+ type UploadQueueItem = {
97
+ id: string;
98
+ file: File;
99
+ name: string;
100
+ size: number;
101
+ status: "queued" | "uploading" | "done" | "failed";
102
+ result?: Awaited<ReturnType<typeof latticeApi.uploadDocument>>;
103
+ };
104
+
105
+ async function uploadFiles(files: File[], setQueue: React.Dispatch<React.SetStateAction<UploadQueueItem[]>>) {
106
+ const rows = files.map((file) => ({
107
+ id: `${file.name}-${file.size}-${file.lastModified}-${Date.now()}`,
108
+ file,
109
+ name: file.name,
110
+ size: file.size,
111
+ status: "queued" as const,
112
+ }));
113
+ setQueue((items) => [...rows, ...items].slice(0, 12));
114
+ const results = [];
115
+ for (const row of rows) {
116
+ setQueue((items) => items.map((item) => item.id === row.id ? { ...item, status: "uploading" } : item));
117
+ const result = await latticeApi.uploadDocument(row.file);
118
+ results.push(result);
119
+ setQueue((items) => items.map((item) => item.id === row.id ? { ...item, status: result.ok ? "done" : "failed", result } : item));
120
+ }
121
+ return results;
122
+ }
123
+
124
+ function DocumentUploadQueue({ queue, onRetry }: { queue: UploadQueueItem[]; onRetry: (file: File) => void }) {
125
+ if (!queue.length) return null;
126
+ return (
127
+ <div className="mt-4 space-y-2">
128
+ {queue.map((item) => {
129
+ const detail = uploadResultDetail(item);
130
+ return (
131
+ <div key={item.id} className="rounded-md border border-border bg-background/55 p-3 text-sm">
132
+ <div className="flex flex-wrap items-start justify-between gap-3">
133
+ <div>
134
+ <div className="flex items-center gap-2 font-medium">
135
+ {item.status === "done" ? <CheckCircle2 className="h-4 w-4 text-emerald-400" /> : item.status === "failed" ? <AlertCircle className="h-4 w-4 text-amber-400" /> : <Loader2 className="h-4 w-4 animate-spin text-primary" />}
136
+ {item.name}
137
+ </div>
138
+ <div className="mt-1 text-xs text-muted-foreground">
139
+ {Math.max(1, Math.round(item.size / 1024))} KB · {detail}
140
+ </div>
141
+ </div>
142
+ {item.status === "failed" ? (
143
+ <Button size="sm" variant="outline" onClick={() => onRetry(item.file)}>
144
+ <RotateCcw className="h-3.5 w-3.5" /> Retry
145
+ </Button>
146
+ ) : null}
147
+ </div>
148
+ {item.result ? <OperationResult result={item.result} successLabel="Entered Brain and graph queue" /> : null}
149
+ </div>
150
+ );
151
+ })}
152
+ </div>
153
+ );
154
+ }
155
+
156
+ function uploadResultDetail(item: UploadQueueItem) {
157
+ if (item.status === "queued") return "Waiting in the ingest queue";
158
+ if (item.status === "uploading") return "Parsing and sending to Memory";
159
+ if (!item.result?.ok) return item.result?.error || "Ingest failed before it entered the Brain";
160
+ const data = item.result.data || {};
161
+ const node = String(data.node_id || data.graph_node || data.provenance_id || "");
162
+ return node ? `Captured with source metadata · ${node}` : "Captured with source metadata";
163
+ }
164
+
76
165
  function LocalPanel() {
77
166
  const qc = useQueryClient();
78
167
  const [path, setPath] = React.useState("");
@@ -109,12 +109,20 @@ function ModelsPanel() {
109
109
  const profile = (data as Record<string, unknown>).profile as Record<string, unknown> | undefined;
110
110
  return (
111
111
  <div className="space-y-4">
112
- <StatGrid stats={[
113
- { label: "Computer", value: profile?.os ? `${String(profile.os)} ${String(profile.arch || "")}` : "detected" },
114
- { label: "Memory", value: recommendation?.ram_gb ? `${String(recommendation.ram_gb)} GB` : "checking" },
115
- { label: "Top pick", value: topPick?.name || topPick?.id || "choose below" },
116
- { label: "Current", value: current?.name || currentId || "none" },
117
- ]} />
112
+ <ModelRuntimeSummary
113
+ profile={profile || {}}
114
+ recommendation={recommendation || {}}
115
+ current={current}
116
+ currentId={currentId}
117
+ latestProgress={latestProgress}
118
+ lastResult={lastResult}
119
+ onReload={() => {
120
+ if (current) void prepareModel(String(current.recommended_load_id || current.id || currentId), String(current.recommended_engine || current.engine || "local_mlx"), consent);
121
+ }}
122
+ onUnload={() => {
123
+ if (currentId) void latticeApi.unloadModel(currentId).then(() => qc.invalidateQueries({ queryKey: ["models"] }));
124
+ }}
125
+ />
118
126
  <div className="grid gap-2 md:grid-cols-3 xl:grid-cols-6">
119
127
  {[
120
128
  ["Environment Analysis", true, Cpu],
@@ -296,6 +304,64 @@ function AlternativeModels({ compatibility }: { compatibility: Record<string, un
296
304
  );
297
305
  }
298
306
 
307
+ function ModelRuntimeSummary({
308
+ profile,
309
+ recommendation,
310
+ current,
311
+ currentId,
312
+ latestProgress,
313
+ lastResult,
314
+ onReload,
315
+ onUnload,
316
+ }: {
317
+ profile: Record<string, unknown>;
318
+ recommendation: Record<string, unknown>;
319
+ current?: Record<string, unknown>;
320
+ currentId: string;
321
+ latestProgress: Record<string, unknown> | null;
322
+ lastResult: Record<string, unknown> | null;
323
+ onReload: () => void;
324
+ onUnload: () => void;
325
+ }) {
326
+ const loadedName = String(current?.name || current?.id || currentId || "");
327
+ const engine = String(current?.engine || current?.recommended_engine || latestProgress?.engine || "local_mlx");
328
+ const cachePath = String(current?.local_path || current?.storage_location || recommendation.cache_path || recommendation.storage_location || "~/.cache/huggingface / ~/.latticeai/models");
329
+ const progressStage = String(latestProgress?.stage || lastResult?.stage || (loadedName ? "ready" : "idle"));
330
+ return (
331
+ <div className="space-y-3">
332
+ <StatGrid stats={[
333
+ { label: "Computer", value: profile?.os ? `${String(profile.os)} ${String(profile.arch || "")}` : "detected" },
334
+ { label: "Engine", value: engine },
335
+ { label: "Loaded model", value: loadedName || "No local model loaded" },
336
+ { label: "Runtime state", value: progressStage },
337
+ ]} />
338
+ <div className="rounded-lg border border-border bg-background/55 p-3 text-sm">
339
+ <div className="flex flex-wrap items-start justify-between gap-3">
340
+ <div>
341
+ <div className="font-medium">{loadedName ? "Local model runtime is available" : "No model is loaded yet"}</div>
342
+ <div className="mt-1 text-muted-foreground">Cache/storage: {cachePath}</div>
343
+ </div>
344
+ <div className="flex flex-wrap gap-2">
345
+ <Button variant="outline" size="sm" disabled={!loadedName} onClick={onReload}>Reload</Button>
346
+ <Button variant="outline" size="sm" disabled={!loadedName} onClick={onUnload}>Unload</Button>
347
+ </div>
348
+ </div>
349
+ {latestProgress ? (
350
+ <div className="mt-3">
351
+ <div className="flex justify-between text-xs text-muted-foreground">
352
+ <span>{String(latestProgress.message || "Preparing model")}</span>
353
+ <span>{Number(latestProgress.percent || 0)}%</span>
354
+ </div>
355
+ <div className="mt-1 h-2 overflow-hidden rounded-full bg-muted">
356
+ <div className="h-full bg-primary" style={{ width: `${Number(latestProgress.percent || 8)}%` }} />
357
+ </div>
358
+ </div>
359
+ ) : null}
360
+ </div>
361
+ </div>
362
+ );
363
+ }
364
+
299
365
  function ModelRecovery({ error }: { error: Record<string, unknown> }) {
300
366
  const guidance = asArray<string>(error.recovery_guidance);
301
367
  return (
@@ -26,7 +26,7 @@ from .storage import (
26
26
  storage_from_env,
27
27
  )
28
28
 
29
- __version__ = "6.2.0"
29
+ __version__ = "6.3.0"
30
30
 
31
31
  __all__ = [
32
32
  "AgentRuntime",
@@ -19,7 +19,7 @@ from datetime import datetime
19
19
  from typing import Any, Callable, Dict, List, Optional
20
20
 
21
21
 
22
- MULTI_AGENT_VERSION = "6.2.0"
22
+ MULTI_AGENT_VERSION = "6.3.0"
23
23
 
24
24
  AGENT_ROLES = ("researcher", "planner", "executor", "reviewer", "release")
25
25
  CORE_PIPELINE = ("planner", "executor", "reviewer")
@@ -1,3 +1,3 @@
1
1
  """Lattice AI - modular server package."""
2
2
 
3
- __version__ = "6.2.0"
3
+ __version__ = "6.3.0"
@@ -20,6 +20,11 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional
20
20
  from latticeai.runtime.app_context_runtime import build_app_context
21
21
  from latticeai.runtime.bootstrap import build_session_runtime
22
22
  from latticeai.runtime.brain_runtime import build_brain_runtime
23
+ from latticeai.runtime.chat_wiring import (
24
+ build_chat_agent_runtime_from_context,
25
+ build_interaction_contexts,
26
+ maybe_build_telegram_chat_mirror,
27
+ )
23
28
  from latticeai.runtime.config_runtime import build_config_runtime
24
29
  from latticeai.runtime.context_runtime import build_context_runtime
25
30
  from latticeai.runtime.hooks_runtime import (
@@ -28,12 +33,16 @@ from latticeai.runtime.hooks_runtime import (
28
33
  build_hooks_runtime,
29
34
  )
30
35
  from latticeai.runtime.lifespan_runtime import build_lifespan_runtime
36
+ from latticeai.runtime.model_wiring import (
37
+ configure_model_runtime_from_context,
38
+ register_model_runtime_routers,
39
+ )
31
40
  from latticeai.runtime.platform_services_runtime import (
32
41
  build_brain_network,
33
- build_model_service,
34
42
  )
35
43
  from latticeai.runtime.platform_runtime_wiring import build_platform_automation_runtime
36
44
  from latticeai.runtime.persistence_runtime import build_persistence_runtime
45
+ from latticeai.runtime.review_wiring import build_review_run_now_runner
37
46
  from latticeai.runtime.router_registration import (
38
47
  build_auth_admin_security_router_bundle,
39
48
  build_static_routes_bundle,
@@ -44,8 +53,8 @@ from latticeai.runtime.router_registration import (
44
53
  register_review_and_brain_tail_routers,
45
54
  )
46
55
  from latticeai.runtime.security_runtime import build_security_runtime
56
+ from latticeai.runtime.tail_wiring import register_tail_runtime_routers
47
57
  from latticeai.runtime.web_runtime import build_web_runtime
48
- from latticeai.services.router_context import InteractionRouterContext, ToolRouterContext
49
58
 
50
59
  if TYPE_CHECKING: # imports for annotations only — keep module import light
51
60
  from fastapi import FastAPI
@@ -1089,7 +1098,8 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1089
1098
  OPEN_REGISTRATION = CONFIG.open_registration
1090
1099
  INVITE_CODE = CONFIG.invite_code
1091
1100
  INVITE_GATE_ENABLED = CONFIG.invite_gate_enabled
1092
- configure_model_runtime(
1101
+ configure_model_runtime_from_context(
1102
+ configure_model_runtime=configure_model_runtime,
1093
1103
  router=router,
1094
1104
  APP_MODE=APP_MODE,
1095
1105
  DEFAULT_HOST=DEFAULT_HOST,
@@ -1236,12 +1246,10 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1236
1246
  # ── Telegram chat mirror: registered only when ENABLE_TELEGRAM is truthy.
1237
1247
  # latticeai.api.chat no longer imports telegram_bot (a 45KB module that
1238
1248
  # mutates os.environ at import); it calls this injected callback instead.
1239
- on_chat_message = None
1240
- if ENABLE_TELEGRAM:
1241
- def _telegram_chat_mirror(role: str, text: str, source: Optional[str] = None) -> None:
1242
- from latticeai.integrations.telegram_bot import broadcast_web_chat
1243
- _spawn(broadcast_web_chat(role, text), name="telegram_broadcast")
1244
- on_chat_message = _telegram_chat_mirror
1249
+ on_chat_message = maybe_build_telegram_chat_mirror(
1250
+ enable_telegram=ENABLE_TELEGRAM,
1251
+ spawn=_spawn,
1252
+ )
1245
1253
 
1246
1254
  def _recent_chat_context(
1247
1255
  limit: int = 10,
@@ -1257,7 +1265,8 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1257
1265
  conversation_id=conversation_id,
1258
1266
  )
1259
1267
 
1260
- CHAT_AGENT_RUNTIME = build_agent_runtime(
1268
+ CHAT_AGENT_RUNTIME = build_chat_agent_runtime_from_context(
1269
+ build_agent_runtime=build_agent_runtime,
1261
1270
  model_router=router,
1262
1271
  execute_tool=execute_tool,
1263
1272
  recent_chat_context=_recent_chat_context,
@@ -1404,22 +1413,19 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1404
1413
  # ── Health / status / engine-summary router (latticeai.api.health, v1.2.0) ───
1405
1414
  # /health, /mode, /runtime_features, /engines(GET) now live in the health router.
1406
1415
  # Heavier engine mutation endpoints remain below in server_app.
1407
- MODEL_SERVICE = build_model_service(
1416
+ MODEL_SERVICE = register_model_runtime_routers(
1417
+ app=app,
1418
+ create_health_router=create_health_router,
1419
+ create_models_router=create_models_router,
1420
+ register_health_and_model_routers=register_health_and_model_routers,
1408
1421
  model_router=router,
1409
1422
  runtime_features=runtime_features,
1410
- is_public=IS_PUBLIC_MODE,
1411
- )
1412
- register_health_and_model_routers(
1413
- app,
1414
- create_health_router=create_health_router,
1415
- model_service=MODEL_SERVICE,
1423
+ is_public_mode=IS_PUBLIC_MODE,
1416
1424
  engine_status=engine_status,
1417
1425
  get_current_user=get_current_user,
1418
1426
  require_auth=REQUIRE_AUTH,
1419
1427
  app_version=APP_VERSION,
1420
1428
  app_mode=APP_MODE,
1421
- create_models_router=create_models_router,
1422
- model_router=router,
1423
1429
  require_user=require_user,
1424
1430
  load_users=load_users,
1425
1431
  get_user_role=get_user_role,
@@ -1438,7 +1444,6 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1438
1444
  engine_model_catalog=ENGINE_MODEL_CATALOG,
1439
1445
  model_engine_aliases=MODEL_ENGINE_ALIASES,
1440
1446
  cloud_verify_ttl_seconds=CLOUD_VERIFY_TTL_SECONDS,
1441
- is_public_mode=IS_PUBLIC_MODE,
1442
1447
  allow_local_models=ALLOW_LOCAL_MODELS,
1443
1448
  )
1444
1449
 
@@ -1461,7 +1466,7 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1461
1466
  return None
1462
1467
  return PLATFORM.allowed_scopes(user)
1463
1468
 
1464
- tool_router_context = ToolRouterContext(
1469
+ tool_router_context, interaction_router_context = build_interaction_contexts(
1465
1470
  config=CONFIG,
1466
1471
  ingestion_pipeline=INGESTION_PIPELINE,
1467
1472
  data_dir=DATA_DIR,
@@ -1485,15 +1490,10 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1485
1490
  install_mcp=install_mcp,
1486
1491
  mcp_public_item=mcp_public_item,
1487
1492
  hooks=HOOKS_REGISTRY,
1488
- )
1489
- interaction_router_context = InteractionRouterContext(
1490
1493
  chat_context=context,
1491
1494
  search_service=SEARCH_SERVICE,
1492
1495
  allowed_workspaces_for=_allowed_workspaces_for,
1493
- require_user=require_user,
1494
1496
  embedding_info=_embedding_info,
1495
- tool_context=tool_router_context,
1496
- hooks=HOOKS_REGISTRY,
1497
1497
  agent_registry=AGENT_REGISTRY,
1498
1498
  memory_service=MEMORY_SERVICE,
1499
1499
  platform=PLATFORM,
@@ -1510,25 +1510,17 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1510
1510
  )
1511
1511
 
1512
1512
  from latticeai.api.review_queue import create_review_queue_router
1513
+ run_review_item = build_review_run_now_runner(PLATFORM, HTTPException)
1513
1514
 
1514
- def _run_review_item(item, *, user_email, scope):
1515
- """run_now: re-execute the suggestion's source workflow (preview/regenerate)."""
1516
- wf_id = (item.get("payload") or {}).get("workflow_id") or (item.get("provenance") or {}).get("workflow_id")
1517
- if not wf_id:
1518
- raise HTTPException(status_code=409, detail="review item has no workflow to run")
1519
- return PLATFORM.run_workflow_by_id(
1520
- wf_id, user_email, scope, with_agent=False,
1521
- inputs={"__review_item__": item.get("id")},
1522
- )
1523
-
1524
- BRAIN_NETWORK = register_review_and_brain_tail_routers(
1525
- app,
1515
+ BRAIN_NETWORK = register_tail_runtime_routers(
1516
+ app=app,
1526
1517
  create_review_queue_router=create_review_queue_router,
1518
+ register_review_and_brain_tail_routers=register_review_and_brain_tail_routers,
1527
1519
  review_queue=REVIEW_QUEUE,
1528
1520
  require_user=require_user,
1529
1521
  gate_read=PLATFORM.gate_read,
1530
1522
  gate_write=PLATFORM.gate_write,
1531
- run_review_item=_run_review_item,
1523
+ run_review_item=run_review_item,
1532
1524
  append_audit_event=append_audit_event,
1533
1525
  create_browser_router=create_browser_router,
1534
1526
  ingestion_pipeline=INGESTION_PIPELINE,
@@ -11,7 +11,7 @@ from copy import deepcopy
11
11
  from typing import Any, Dict, List, Optional
12
12
 
13
13
 
14
- MARKETPLACE_VERSION = "6.2.0"
14
+ MARKETPLACE_VERSION = "6.3.0"
15
15
  TEMPLATE_KINDS = ("plugin", "workflow", "agent")
16
16
 
17
17
 
@@ -19,7 +19,7 @@ from pathlib import Path
19
19
  from typing import Any, Callable, Dict, Iterable, List, Optional
20
20
 
21
21
 
22
- WORKSPACE_OS_VERSION = "6.2.0"
22
+ WORKSPACE_OS_VERSION = "6.3.0"
23
23
 
24
24
  # Workspace types separate single-user Personal workspaces from shared
25
25
  # Organization workspaces. Both keep the same local-first JSON store; the type
@@ -0,0 +1,119 @@
1
+ """Chat and interaction wiring seam for ``latticeai.app_factory``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Optional
6
+
7
+ from latticeai.services.router_context import InteractionRouterContext, ToolRouterContext
8
+
9
+
10
+ def build_chat_agent_runtime_from_context(
11
+ *,
12
+ build_agent_runtime: Any,
13
+ model_router: Any,
14
+ execute_tool: Any,
15
+ recent_chat_context: Any,
16
+ clear_history: Any,
17
+ knowledge_save: Any,
18
+ audit: Any,
19
+ hooks: Any,
20
+ brain_memory: Any,
21
+ ) -> Any:
22
+ return build_agent_runtime(
23
+ model_router=model_router,
24
+ execute_tool=execute_tool,
25
+ recent_chat_context=recent_chat_context,
26
+ clear_history=clear_history,
27
+ knowledge_save=knowledge_save,
28
+ audit=audit,
29
+ hooks=hooks,
30
+ brain_memory=brain_memory,
31
+ )
32
+
33
+
34
+ def build_interaction_contexts(
35
+ *,
36
+ config: Any,
37
+ ingestion_pipeline: Any,
38
+ data_dir: Any,
39
+ static_dir: Any,
40
+ model_router: Any,
41
+ require_user: Any,
42
+ require_admin: Any,
43
+ get_current_user: Any,
44
+ clear_history: Any,
45
+ append_audit_event: Any,
46
+ enforce_rate_limit: Any,
47
+ bytes_match_extension: Any,
48
+ classify_sensitive_message: Any,
49
+ save_to_history: Any,
50
+ enable_graph: bool,
51
+ knowledge_graph: Any,
52
+ require_graph: Any,
53
+ local_kg_watcher: Any,
54
+ load_mcp_installs: Any,
55
+ recommend_mcps: Any,
56
+ install_mcp: Any,
57
+ mcp_public_item: Any,
58
+ hooks: Any,
59
+ chat_context: Any,
60
+ search_service: Any,
61
+ allowed_workspaces_for: Any,
62
+ embedding_info: Any,
63
+ agent_registry: Any,
64
+ memory_service: Any,
65
+ platform: Any,
66
+ ) -> tuple[ToolRouterContext, InteractionRouterContext]:
67
+ tool_router_context = ToolRouterContext(
68
+ config=config,
69
+ ingestion_pipeline=ingestion_pipeline,
70
+ data_dir=data_dir,
71
+ static_dir=static_dir,
72
+ model_router=model_router,
73
+ require_user=require_user,
74
+ require_admin=require_admin,
75
+ get_current_user=get_current_user,
76
+ clear_history=clear_history,
77
+ append_audit_event=append_audit_event,
78
+ enforce_rate_limit=enforce_rate_limit,
79
+ bytes_match_extension=bytes_match_extension,
80
+ classify_sensitive_message=classify_sensitive_message,
81
+ save_to_history=save_to_history,
82
+ enable_graph=enable_graph,
83
+ knowledge_graph=knowledge_graph,
84
+ require_graph=require_graph,
85
+ local_kg_watcher=local_kg_watcher,
86
+ load_mcp_installs=load_mcp_installs,
87
+ recommend_mcps=recommend_mcps,
88
+ install_mcp=install_mcp,
89
+ mcp_public_item=mcp_public_item,
90
+ hooks=hooks,
91
+ )
92
+ interaction_router_context = InteractionRouterContext(
93
+ chat_context=chat_context,
94
+ search_service=search_service,
95
+ allowed_workspaces_for=allowed_workspaces_for,
96
+ require_user=require_user,
97
+ embedding_info=embedding_info,
98
+ tool_context=tool_router_context,
99
+ hooks=hooks,
100
+ agent_registry=agent_registry,
101
+ memory_service=memory_service,
102
+ platform=platform,
103
+ )
104
+ return tool_router_context, interaction_router_context
105
+
106
+
107
+ def maybe_build_telegram_chat_mirror(
108
+ *,
109
+ enable_telegram: bool,
110
+ spawn: Any,
111
+ ) -> Optional[Any]:
112
+ if not enable_telegram:
113
+ return None
114
+
115
+ def telegram_chat_mirror(role: str, text: str, source: Optional[str] = None) -> None:
116
+ from latticeai.integrations.telegram_bot import broadcast_web_chat
117
+ spawn(broadcast_web_chat(role, text), name="telegram_broadcast")
118
+
119
+ return telegram_chat_mirror
@@ -0,0 +1,40 @@
1
+ """Model/runtime wiring seam for ``latticeai.app_factory``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from latticeai.runtime.platform_services_runtime import build_model_service
8
+
9
+
10
+ def configure_model_runtime_from_context(**kwargs: Any) -> None:
11
+ configure_model_runtime = kwargs.pop("configure_model_runtime")
12
+ configure_model_runtime(**kwargs)
13
+
14
+
15
+ def register_model_runtime_routers(
16
+ *,
17
+ app: Any,
18
+ create_health_router: Any,
19
+ create_models_router: Any,
20
+ register_health_and_model_routers: Any,
21
+ model_router: Any,
22
+ runtime_features: Any,
23
+ is_public_mode: bool,
24
+ **kwargs: Any,
25
+ ) -> Any:
26
+ model_service = build_model_service(
27
+ model_router=model_router,
28
+ runtime_features=runtime_features,
29
+ is_public=is_public_mode,
30
+ )
31
+ register_health_and_model_routers(
32
+ app,
33
+ create_health_router=create_health_router,
34
+ model_service=model_service,
35
+ create_models_router=create_models_router,
36
+ model_router=model_router,
37
+ is_public_mode=is_public_mode,
38
+ **kwargs,
39
+ )
40
+ return model_service
@@ -9,9 +9,6 @@ from __future__ import annotations
9
9
 
10
10
  from typing import Any, Callable, Dict
11
11
 
12
- from latticeai.runtime.automation_runtime import build_automation_runtime
13
- from latticeai.services.platform_runtime import PlatformRuntime
14
-
15
12
 
16
13
  def build_platform_automation_runtime(
17
14
  *,
@@ -34,6 +31,8 @@ def build_platform_automation_runtime(
34
31
  ``app_factory._build`` so ``dict(locals())`` continues to expose the legacy
35
32
  ``server_app`` compatibility surface.
36
33
  """
34
+ from latticeai.runtime.automation_runtime import build_automation_runtime
35
+ from latticeai.services.platform_runtime import PlatformRuntime
37
36
 
38
37
  def _llm_generate_sync(
39
38
  message: str,
@@ -0,0 +1,37 @@
1
+ """Review Center runtime wiring helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Callable, Dict, Optional
6
+
7
+
8
+ def build_review_run_now_runner(platform: Any, http_exception: type[Exception]) -> Callable[..., Any]:
9
+ """Build the Review Center run-now runner used by the API router.
10
+
11
+ The runner preserves the public contract: "Run now" previews/regenerates the
12
+ source workflow, records a fresh run id, and leaves approval status unchanged.
13
+ """
14
+
15
+ def run_review_item(
16
+ item: Dict[str, Any],
17
+ *,
18
+ user_email: Optional[str],
19
+ scope: Optional[str],
20
+ ) -> Any:
21
+ payload = item.get("payload") or {}
22
+ provenance = item.get("provenance") or {}
23
+ workflow_id = payload.get("workflow_id") or provenance.get("workflow_id")
24
+ if not workflow_id:
25
+ raise http_exception(status_code=409, detail="review item has no workflow to run")
26
+ return platform.run_workflow_by_id(
27
+ workflow_id,
28
+ user_email,
29
+ scope,
30
+ with_agent=False,
31
+ inputs={"__review_item__": item.get("id")},
32
+ )
33
+
34
+ return run_review_item
35
+
36
+
37
+ __all__ = ["build_review_run_now_runner"]