ltcai 11.1.0 → 11.2.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 +53 -53
- package/docs/CHANGELOG.md +33 -0
- package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
- package/docs/DEVELOPMENT.md +1 -1
- package/docs/FEATURE_AUDIT_v11.2.0.md +393 -0
- package/docs/LAYOUT_REBUILD_SPEC.md +9 -1
- package/docs/ONBOARDING.md +1 -1
- package/docs/OPERATIONS.md +1 -1
- package/docs/TRUST_MODEL.md +1 -1
- package/docs/WHY_LATTICE.md +1 -1
- package/docs/architecture.md +6 -2
- package/docs/kg-schema.md +1 -1
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/gates.py +125 -0
- package/lattice_brain/graph/fusion.py +35 -4
- package/lattice_brain/graph/projection.py +66 -8
- package/lattice_brain/graph/schema.py +9 -0
- package/lattice_brain/graph/store.py +9 -0
- package/lattice_brain/graph/vector_index/selector.py +32 -2
- package/lattice_brain/ingestion.py +175 -27
- package/lattice_brain/multimodal.py +525 -5
- package/lattice_brain/portability.py +169 -32
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/lattice_brain/sealed_box.py +244 -0
- package/lattice_brain/synthesis.py +24 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/brain_intelligence.py +4 -0
- package/latticeai/api/chat.py +11 -0
- package/latticeai/api/chat_helpers.py +16 -3
- package/latticeai/api/chat_hybrid.py +32 -1
- package/latticeai/api/features.py +70 -0
- package/latticeai/api/local_files.py +102 -0
- package/latticeai/api/portability.py +39 -4
- package/latticeai/api/review_queue.py +126 -0
- package/latticeai/api/search.py +16 -2
- package/latticeai/core/agent.py +55 -2
- package/latticeai/core/config.py +4 -1
- package/latticeai/core/context_builder.py +6 -3
- package/latticeai/core/legacy_compatibility.py +1 -1
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/messages.py +143 -0
- package/latticeai/core/model_compat.py +73 -2
- package/latticeai/core/workspace_os_constants.py +1 -1
- package/latticeai/models/model_providers.py +12 -4
- package/latticeai/runtime/build_phases.py +28 -0
- package/latticeai/runtime/chat_wiring.py +4 -0
- package/latticeai/runtime/feature_toggle_wiring.py +163 -0
- package/latticeai/runtime/router_registration.py +11 -0
- package/latticeai/services/app_context.py +8 -0
- package/latticeai/services/architecture_readiness.py +1 -1
- package/latticeai/services/automation_intelligence.py +22 -2
- package/latticeai/services/brain_intelligence.py +123 -7
- package/latticeai/services/command_center.py +10 -4
- package/latticeai/services/feature_toggles.py +502 -0
- package/latticeai/services/folder_watch.py +122 -1
- package/latticeai/services/hybrid_chat.py +56 -5
- package/latticeai/services/interop_bridges.py +978 -0
- package/latticeai/services/model_capability_registry.py +434 -261
- package/latticeai/services/model_catalog.py +95 -61
- package/latticeai/services/model_recommendation.py +18 -11
- package/latticeai/services/model_runtime.py +1 -1
- package/latticeai/services/multimodal_ports.py +26 -1
- package/latticeai/services/obsidian_bridge.py +16 -25
- package/latticeai/services/product_readiness.py +1 -1
- package/latticeai/services/search_service.py +149 -2
- package/latticeai/services/tool_dispatch.py +4 -0
- package/latticeai/setup/auto_setup.py +27 -30
- package/latticeai/setup/wizard.py +77 -44
- package/package.json +1 -1
- package/scripts/check_current_release_docs.mjs +1 -1
- package/scripts/check_server_i18n.mjs +1 -0
- package/scripts/release_screen_claims.json +13 -0
- package/scripts/verify_hf_model_registry.py +253 -218
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/tauri.conf.json +1 -1
- package/static/app/asset-manifest.json +37 -37
- package/static/app/assets/{Act-D0HWqtn0.js → Act-AWf0SAKp.js} +1 -1
- package/static/app/assets/{AdminConsole-D-QDW-A4.js → AdminConsole-D0u8Tiyj.js} +1 -1
- package/static/app/assets/{Brain-CzCsI1mi.js → Brain-tuhI4sOC.js} +1 -1
- package/static/app/assets/BrainHome-Ts7G_Ila.js +2 -0
- package/static/app/assets/{BrainSignals-2dHQNkns.js → BrainSignals-jMYgQ2Ar.js} +1 -1
- package/static/app/assets/{Capture-CT8v1StE.js → Capture-CqOSzyPr.js} +1 -1
- package/static/app/assets/{CommandPalette-DoLXC2KH.js → CommandPalette-DC0Bzh-I.js} +1 -1
- package/static/app/assets/{Library-DDoxFE5c.js → Library-CX-bbhmK.js} +1 -1
- package/static/app/assets/{LivingBrain-BXMWIK_2.js → LivingBrain-DBwhto14.js} +1 -1
- package/static/app/assets/{ProductFlow-DOYf7JIs.js → ProductFlow-BHA2cfKI.js} +1 -1
- package/static/app/assets/{ReviewCard-COQsqidK.js → ReviewCard-BUhCKRNM.js} +1 -1
- package/static/app/assets/{System-BRllvYXd.js → System-Bu2t5hn1.js} +1 -1
- package/static/app/assets/arrow-left-Dzwa5zRb.js +1 -0
- package/static/app/assets/{bot-4BvN07ux.js → bot-Cia42c2h.js} +1 -1
- package/static/app/assets/brain-DJMoqrwx.js +1 -0
- package/static/app/assets/{button-CDjtnAoU.js → button-2j2Ijzgq.js} +1 -1
- package/static/app/assets/{circle-pause-D_RMn7tp.js → circle-pause-BEFeWpVW.js} +1 -1
- package/static/app/assets/{circle-play-B5OpB8ae.js → circle-play-ujXMcHxl.js} +1 -1
- package/static/app/assets/{cpu-BIlWInHf.js → cpu-k4awryFq.js} +1 -1
- package/static/app/assets/{download-BtjXfL3z.js → download-DFbLJ_ig.js} +1 -1
- package/static/app/assets/{folder-open-DefMpxI2.js → folder-open-7y_b6xkM.js} +1 -1
- package/static/app/assets/{hard-drive-BQ8NZVkw.js → hard-drive-Bidh02Kr.js} +1 -1
- package/static/app/assets/{index-0AvoEBzJ.js → index-BpYkzcVm.js} +3 -3
- package/static/app/assets/{index-vtEfYvQY.css → index-DwDl9-8Y.css} +1 -1
- package/static/app/assets/{input-B_5ZJ9oy.js → input-DSlJJxRs.js} +1 -1
- package/static/app/assets/{permissionCopy-BqZ5tsgL.js → permissionCopy-Bpb83Hx9.js} +1 -1
- package/static/app/assets/{primitives-CVwew78r.js → primitives-BCx6TvfG.js} +1 -1
- package/static/app/assets/search-Cgy8cCFJ.js +1 -0
- package/static/app/assets/{share-2-D5zg_0fY.js → share-2-BH1M-WNi.js} +1 -1
- package/static/app/assets/{shield-alert-B5pZzkUb.js → shield-alert-BlKdBXcG.js} +1 -1
- package/static/app/assets/{textarea-nEVIweKY.js → textarea-CCWbUfFB.js} +1 -1
- package/static/app/assets/{useFocusTrap-Cm99AHlz.js → useFocusTrap-YdHQ7pJ1.js} +1 -1
- package/static/app/assets/{useQuery-Dm__N6bL.js → useQuery-CXQiwbVT.js} +1 -1
- package/static/app/assets/{utils-DcDMoZIe.js → utils-zqPZJxdx.js} +2 -2
- package/static/app/assets/{workspace-LtRRSKTf.js → workspace-DXTihhfU.js} +1 -1
- package/static/app/index.html +4 -4
- package/static/sw.js +1 -1
- package/static/app/assets/BrainHome-Btns-_TA.js +0 -2
- package/static/app/assets/arrow-left-DnyMzss-.js +0 -1
- package/static/app/assets/brain-uMb_5hnO.js +0 -1
- package/static/app/assets/search-DkhnOKZt.js +0 -1
|
@@ -29,6 +29,7 @@ import uuid
|
|
|
29
29
|
from pathlib import Path
|
|
30
30
|
from typing import Any, Dict, List, Optional
|
|
31
31
|
|
|
32
|
+
from lattice_brain.gates import FeatureGate
|
|
32
33
|
from lattice_brain.ingestion import (
|
|
33
34
|
DEFAULT_FOLDER_EXTENSIONS,
|
|
34
35
|
DEFAULT_MAX_FILE_BYTES,
|
|
@@ -46,6 +47,33 @@ WATCH_INTERVAL_ENV = "LATTICEAI_FOLDER_WATCH_INTERVAL"
|
|
|
46
47
|
DEFAULT_WATCH_INTERVAL_SECONDS = 30.0
|
|
47
48
|
MAX_FILES_PER_SCAN = 200 # per-watch per-scan ingest cap (thrash guard)
|
|
48
49
|
|
|
50
|
+
# ── vault watch (v11.2.0) ────────────────────────────────────────────────────
|
|
51
|
+
#: A watched *vault* is not a watched folder. 11.1.0 refused to schedule vault
|
|
52
|
+
#: syncs for a real reason — link edges need the node ids that only a completed
|
|
53
|
+
#: inline ingest has, so a per-file incremental pass would report edges it never
|
|
54
|
+
#: wrote. The resolution is not to make the bridge incremental: it is to notice
|
|
55
|
+
#: that the vault changed and then run the **whole** bridge sync, which does
|
|
56
|
+
#: produce those edges and is idempotent by content hash. What the snapshot buys
|
|
57
|
+
#: is skipping the sync entirely when nothing moved.
|
|
58
|
+
WATCH_KIND_FOLDER = "folder"
|
|
59
|
+
WATCH_KIND_VAULT = "vault"
|
|
60
|
+
WATCH_KINDS = (WATCH_KIND_FOLDER, WATCH_KIND_VAULT)
|
|
61
|
+
VAULT_WATCH_ENV = "LATTICEAI_VAULT_WATCH"
|
|
62
|
+
#: Opt-in, default off, resolved when asked so a settings toggle can move it.
|
|
63
|
+
VAULT_WATCH_GATE = FeatureGate(
|
|
64
|
+
VAULT_WATCH_ENV,
|
|
65
|
+
default=False,
|
|
66
|
+
name="vault_watch",
|
|
67
|
+
detail=(
|
|
68
|
+
"Watching an external vault re-syncs it in the background; it is off "
|
|
69
|
+
"until you turn it on."
|
|
70
|
+
),
|
|
71
|
+
)
|
|
72
|
+
VAULT_WATCH_DISABLED_DETAIL = (
|
|
73
|
+
"vault watch is off by default; turn it on to let an external vault "
|
|
74
|
+
f"re-sync in the background ({VAULT_WATCH_ENV})"
|
|
75
|
+
)
|
|
76
|
+
|
|
49
77
|
|
|
50
78
|
def _default_interval() -> float:
|
|
51
79
|
raw = os.getenv(WATCH_INTERVAL_ENV, "").strip()
|
|
@@ -65,8 +93,13 @@ class FolderWatchService:
|
|
|
65
93
|
pipeline: Any,
|
|
66
94
|
config_path: Path,
|
|
67
95
|
interval_seconds: Optional[float] = None,
|
|
96
|
+
vault_bridge: Optional[Any] = None,
|
|
68
97
|
) -> None:
|
|
69
98
|
self._pipeline = pipeline
|
|
99
|
+
# Injected rather than imported at call time so a vault watch can be
|
|
100
|
+
# driven by a test double, and so this module keeps knowing nothing
|
|
101
|
+
# about how a vault is parsed.
|
|
102
|
+
self._vault_bridge = vault_bridge
|
|
70
103
|
self._config_path = Path(config_path)
|
|
71
104
|
self._interval = float(interval_seconds) if interval_seconds else _default_interval()
|
|
72
105
|
self._lock = threading.Lock()
|
|
@@ -100,13 +133,23 @@ class FolderWatchService:
|
|
|
100
133
|
owner: Optional[str] = None,
|
|
101
134
|
workspace_id: Optional[str] = None,
|
|
102
135
|
recursive: bool = True,
|
|
136
|
+
kind: str = WATCH_KIND_FOLDER,
|
|
103
137
|
) -> Dict[str, Any]:
|
|
104
138
|
"""Explicit opt-in: snapshot the folder now, ingest only future changes.
|
|
105
139
|
|
|
106
140
|
The folder is assumed to have been ingested already (the UI offers
|
|
107
141
|
watch after a folder ingest); the enable-time snapshot is the baseline,
|
|
108
142
|
so enabling never re-ingests the whole folder.
|
|
143
|
+
|
|
144
|
+
``kind="vault"`` watches an external Obsidian vault instead, re-running
|
|
145
|
+
the whole bridge sync when the snapshot moves. That mode carries its own
|
|
146
|
+
opt-in gate on top of this one and refuses while it is off.
|
|
109
147
|
"""
|
|
148
|
+
kind = str(kind or WATCH_KIND_FOLDER).strip().lower()
|
|
149
|
+
if kind not in WATCH_KINDS:
|
|
150
|
+
return {"status": "failed", "detail": f"unknown watch kind: {kind}"}
|
|
151
|
+
if kind == WATCH_KIND_VAULT and not VAULT_WATCH_GATE.enabled():
|
|
152
|
+
return {"status": "disabled", "detail": VAULT_WATCH_DISABLED_DETAIL}
|
|
110
153
|
root = Path(path).expanduser()
|
|
111
154
|
if not root.is_dir():
|
|
112
155
|
return {"status": "failed", "detail": f"not a directory: {root}"}
|
|
@@ -116,6 +159,7 @@ class FolderWatchService:
|
|
|
116
159
|
same_scope = (watch.get("workspace_id") or None) == (workspace_id or None)
|
|
117
160
|
if watch.get("path") == str(root) and same_scope:
|
|
118
161
|
watch["enabled"] = True
|
|
162
|
+
watch["kind"] = kind
|
|
119
163
|
watch["snapshot"] = self._snapshot(root, recursive=bool(watch.get("recursive", True)))
|
|
120
164
|
self._save_config()
|
|
121
165
|
self._ensure_thread()
|
|
@@ -127,6 +171,7 @@ class FolderWatchService:
|
|
|
127
171
|
"owner": owner,
|
|
128
172
|
"workspace_id": workspace_id,
|
|
129
173
|
"recursive": bool(recursive),
|
|
174
|
+
"kind": kind,
|
|
130
175
|
"enabled": True,
|
|
131
176
|
"created_at": _now_iso(),
|
|
132
177
|
"last_scan_at": None,
|
|
@@ -172,6 +217,11 @@ class FolderWatchService:
|
|
|
172
217
|
"polling": polling,
|
|
173
218
|
"interval_seconds": self._interval,
|
|
174
219
|
"watches": watches,
|
|
220
|
+
"kinds": list(WATCH_KINDS),
|
|
221
|
+
"vault_watch": {
|
|
222
|
+
**VAULT_WATCH_GATE.describe(),
|
|
223
|
+
"bridge_wired": self._vault_bridge is not None,
|
|
224
|
+
},
|
|
175
225
|
"note": (
|
|
176
226
|
"Watch mode is opt-in and off by default. Deleted files are "
|
|
177
227
|
"counted but never auto-removed from the Brain."
|
|
@@ -208,10 +258,12 @@ class FolderWatchService:
|
|
|
208
258
|
recursive = bool(watch.get("recursive", True))
|
|
209
259
|
owner = watch.get("owner")
|
|
210
260
|
workspace_id = watch.get("workspace_id")
|
|
261
|
+
kind = str(watch.get("kind") or WATCH_KIND_FOLDER)
|
|
211
262
|
|
|
212
263
|
result: Dict[str, Any] = {
|
|
213
264
|
"status": "ok",
|
|
214
265
|
"watch_id": watch_id,
|
|
266
|
+
"kind": kind,
|
|
215
267
|
"new": 0,
|
|
216
268
|
"changed": 0,
|
|
217
269
|
"removed": 0,
|
|
@@ -237,6 +289,17 @@ class FolderWatchService:
|
|
|
237
289
|
changed_rel.append(rel)
|
|
238
290
|
result["removed"] = sum(1 for rel in snapshot if rel not in current)
|
|
239
291
|
|
|
292
|
+
if kind == WATCH_KIND_VAULT:
|
|
293
|
+
self._vault_scan(
|
|
294
|
+
result,
|
|
295
|
+
root=root,
|
|
296
|
+
changed=bool(changed_rel) or bool(result["removed"]),
|
|
297
|
+
owner=owner,
|
|
298
|
+
workspace_id=workspace_id,
|
|
299
|
+
)
|
|
300
|
+
self._record_scan(watch_id, result, snapshot=current)
|
|
301
|
+
return result
|
|
302
|
+
|
|
240
303
|
for rel in changed_rel[:MAX_FILES_PER_SCAN]:
|
|
241
304
|
path = root / rel
|
|
242
305
|
ext = path.suffix.lower()
|
|
@@ -287,6 +350,53 @@ class FolderWatchService:
|
|
|
287
350
|
return result
|
|
288
351
|
|
|
289
352
|
# ── internals ────────────────────────────────────────────────────────────
|
|
353
|
+
def _vault_scan(
|
|
354
|
+
self,
|
|
355
|
+
result: Dict[str, Any],
|
|
356
|
+
*,
|
|
357
|
+
root: Path,
|
|
358
|
+
changed: bool,
|
|
359
|
+
owner: Optional[str],
|
|
360
|
+
workspace_id: Optional[str],
|
|
361
|
+
) -> None:
|
|
362
|
+
"""Re-run the whole vault sync — but only when something actually moved.
|
|
363
|
+
|
|
364
|
+
A full sync is what makes the link edges real; the snapshot is only
|
|
365
|
+
there to avoid paying for one when the vault is untouched. The gate is
|
|
366
|
+
re-checked here, not just at enable time, so turning vault watch off
|
|
367
|
+
stops the background work immediately instead of at the next restart.
|
|
368
|
+
"""
|
|
369
|
+
if not VAULT_WATCH_GATE.enabled():
|
|
370
|
+
result.update(status="disabled", detail=VAULT_WATCH_DISABLED_DETAIL)
|
|
371
|
+
return
|
|
372
|
+
if not changed:
|
|
373
|
+
result["synced"] = False
|
|
374
|
+
result["detail"] = "vault unchanged since the last scan"
|
|
375
|
+
return
|
|
376
|
+
if self._vault_bridge is None:
|
|
377
|
+
result.update(
|
|
378
|
+
status="failed",
|
|
379
|
+
synced=False,
|
|
380
|
+
detail="no vault bridge is wired; the vault was not re-synced",
|
|
381
|
+
)
|
|
382
|
+
return
|
|
383
|
+
try:
|
|
384
|
+
summary = self._vault_bridge.sync(
|
|
385
|
+
str(root), owner=owner, workspace_id=workspace_id, user_email=owner,
|
|
386
|
+
)
|
|
387
|
+
except Exception as exc: # noqa: BLE001 — one bad sync must not kill the poller
|
|
388
|
+
result.update(status="failed", synced=False, detail=f"vault sync failed: {exc}")
|
|
389
|
+
return
|
|
390
|
+
result["synced"] = True
|
|
391
|
+
result["ingested"] = int(summary.get("ingested") or 0)
|
|
392
|
+
result["duplicate"] = int(summary.get("duplicate") or 0)
|
|
393
|
+
result["failed"] = int(summary.get("failed") or 0)
|
|
394
|
+
result["errors"] = list(summary.get("errors") or [])[:20]
|
|
395
|
+
result["edges"] = summary.get("edges")
|
|
396
|
+
result["status"] = "ok" if summary.get("status") in ("ok", "dry_run") else str(
|
|
397
|
+
summary.get("status") or "ok"
|
|
398
|
+
)
|
|
399
|
+
|
|
290
400
|
def _record_scan(
|
|
291
401
|
self,
|
|
292
402
|
watch_id: str,
|
|
@@ -355,6 +465,7 @@ class FolderWatchService:
|
|
|
355
465
|
"owner": watch.get("owner"),
|
|
356
466
|
"workspace_id": watch.get("workspace_id"),
|
|
357
467
|
"recursive": bool(watch.get("recursive", True)),
|
|
468
|
+
"kind": str(watch.get("kind") or WATCH_KIND_FOLDER),
|
|
358
469
|
"enabled": bool(watch.get("enabled")),
|
|
359
470
|
"created_at": watch.get("created_at"),
|
|
360
471
|
"last_scan_at": watch.get("last_scan_at"),
|
|
@@ -396,4 +507,14 @@ class FolderWatchService:
|
|
|
396
507
|
continue
|
|
397
508
|
|
|
398
509
|
|
|
399
|
-
__all__ = [
|
|
510
|
+
__all__ = [
|
|
511
|
+
"DEFAULT_WATCH_INTERVAL_SECONDS",
|
|
512
|
+
"VAULT_WATCH_DISABLED_DETAIL",
|
|
513
|
+
"VAULT_WATCH_ENV",
|
|
514
|
+
"VAULT_WATCH_GATE",
|
|
515
|
+
"WATCH_INTERVAL_ENV",
|
|
516
|
+
"WATCH_KINDS",
|
|
517
|
+
"WATCH_KIND_FOLDER",
|
|
518
|
+
"WATCH_KIND_VAULT",
|
|
519
|
+
"FolderWatchService",
|
|
520
|
+
]
|
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
Phase 1–2 entry point used by the chat path when NetworkBoundaryMode is
|
|
4
4
|
CLOUD_ALLOWED. Local-only sessions never enter this module's cloud path.
|
|
5
|
+
|
|
6
|
+
Both turn functions take the Review Center sink and the hybrid policy's
|
|
7
|
+
``auto_commit`` decision as arguments rather than reaching for them: the
|
|
8
|
+
caller that knows *whose* turn this is is the only one that can resolve a
|
|
9
|
+
per-user policy, and a service that reads a process singleton cannot be
|
|
10
|
+
tested against both branches. Defaults are the safe ones — no sink, no
|
|
11
|
+
auto-commit — so a headless caller stages nothing rather than writing.
|
|
5
12
|
"""
|
|
6
13
|
|
|
7
14
|
from __future__ import annotations
|
|
@@ -36,6 +43,35 @@ def _scope_key(user_email: Optional[str], workspace_id: Optional[str]) -> str:
|
|
|
36
43
|
return f"{user_email or 'anon'}|{workspace_id or 'global'}"
|
|
37
44
|
|
|
38
45
|
|
|
46
|
+
def _ingest_cloud_expansion(
|
|
47
|
+
result: CloudTurnResult,
|
|
48
|
+
*,
|
|
49
|
+
knowledge_graph: Any,
|
|
50
|
+
review_queue: Any,
|
|
51
|
+
auto_commit: bool,
|
|
52
|
+
user_email: Optional[str],
|
|
53
|
+
workspace_id: Optional[str],
|
|
54
|
+
) -> Dict[str, Any]:
|
|
55
|
+
"""Stage what the cloud answer taught the Brain (v11.2.0 wiring).
|
|
56
|
+
|
|
57
|
+
``plan_kg_expansion_rich`` builds every plan with ``auto_commit=False``
|
|
58
|
+
because extraction has no idea what the user consented to; the policy dial
|
|
59
|
+
does, and this is where the two meet. With the sink bound the plan lands in
|
|
60
|
+
the Review Center as a ``change_proposal``, which is what makes
|
|
61
|
+
cloud-derived memory growth a thing the user approves rather than a thing
|
|
62
|
+
that happens to them.
|
|
63
|
+
"""
|
|
64
|
+
plan = plan_kg_expansion_rich(result)
|
|
65
|
+
plan.auto_commit = bool(auto_commit)
|
|
66
|
+
ingestor = CloudResponseIngestor(
|
|
67
|
+
store=knowledge_graph,
|
|
68
|
+
review_queue=review_queue,
|
|
69
|
+
user_email=user_email,
|
|
70
|
+
workspace_id=workspace_id,
|
|
71
|
+
)
|
|
72
|
+
return ingestor.ingest(plan)
|
|
73
|
+
|
|
74
|
+
|
|
39
75
|
async def run_hybrid_cloud_turn(
|
|
40
76
|
*,
|
|
41
77
|
user_message: str,
|
|
@@ -46,6 +82,8 @@ async def run_hybrid_cloud_turn(
|
|
|
46
82
|
model: Optional[str] = None,
|
|
47
83
|
top_k: int = 6,
|
|
48
84
|
adapter: Optional[Any] = None,
|
|
85
|
+
review_queue: Any = None,
|
|
86
|
+
auto_commit: bool = False,
|
|
49
87
|
) -> CloudTurnResult:
|
|
50
88
|
"""Non-streaming helper: full answer + expansion plan."""
|
|
51
89
|
mode = normalize_network_mode(mode)
|
|
@@ -85,9 +123,14 @@ async def run_hybrid_cloud_turn(
|
|
|
85
123
|
mode=mode,
|
|
86
124
|
model=model,
|
|
87
125
|
)
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
126
|
+
ingest_status = _ingest_cloud_expansion(
|
|
127
|
+
result,
|
|
128
|
+
knowledge_graph=knowledge_graph,
|
|
129
|
+
review_queue=review_queue,
|
|
130
|
+
auto_commit=auto_commit,
|
|
131
|
+
user_email=user_email,
|
|
132
|
+
workspace_id=workspace_id,
|
|
133
|
+
)
|
|
91
134
|
used = minimal.token_estimate + max(1, len(result.answer_text) // 4)
|
|
92
135
|
budget.record(used)
|
|
93
136
|
result.usage = {
|
|
@@ -116,6 +159,8 @@ async def stream_hybrid_cloud_turn(
|
|
|
116
159
|
history_user: Optional[Dict[str, Any]] = None,
|
|
117
160
|
notify: Any = None,
|
|
118
161
|
source: Optional[str] = None,
|
|
162
|
+
review_queue: Any = None,
|
|
163
|
+
auto_commit: bool = False,
|
|
119
164
|
) -> AsyncIterator[str]:
|
|
120
165
|
"""SSE generator for a hybrid cloud turn (Phase 2 chat path).
|
|
121
166
|
|
|
@@ -220,8 +265,14 @@ async def stream_hybrid_cloud_turn(
|
|
|
220
265
|
}
|
|
221
266
|
)
|
|
222
267
|
|
|
223
|
-
|
|
224
|
-
|
|
268
|
+
ingest_status = _ingest_cloud_expansion(
|
|
269
|
+
result,
|
|
270
|
+
knowledge_graph=knowledge_graph,
|
|
271
|
+
review_queue=review_queue,
|
|
272
|
+
auto_commit=auto_commit,
|
|
273
|
+
user_email=user_email,
|
|
274
|
+
workspace_id=workspace_id,
|
|
275
|
+
)
|
|
225
276
|
used = minimal.token_estimate + max(1, len(result.answer_text) // 4)
|
|
226
277
|
budget.record(used)
|
|
227
278
|
|