ltcai 9.9.2 → 9.9.3
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 +39 -39
- package/docs/CHANGELOG.md +44 -0
- package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
- package/docs/DEVELOPMENT.md +1 -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/kg-schema.md +1 -1
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/graph/curator.py +141 -0
- package/lattice_brain/graph/documents.py +111 -0
- package/lattice_brain/graph/fusion.py +179 -0
- package/lattice_brain/graph/projection.py +161 -0
- package/lattice_brain/graph/retrieval.py +19 -1
- package/lattice_brain/ingestion.py +66 -0
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/automation_intelligence.py +159 -0
- package/latticeai/api/browser.py +15 -4
- package/latticeai/api/chat.py +27 -2
- package/latticeai/api/chat_agent_http.py +191 -3
- package/latticeai/api/chat_contracts.py +7 -1
- package/latticeai/api/chat_helpers.py +147 -0
- package/latticeai/api/chat_intents.py +189 -1
- package/latticeai/api/chat_stream.py +17 -1
- package/latticeai/api/funnel_metrics.py +31 -0
- package/latticeai/api/knowledge_graph.py +30 -0
- package/latticeai/api/local_files.py +90 -0
- package/latticeai/api/setup.py +134 -1
- package/latticeai/api/tools.py +33 -3
- package/latticeai/app_factory.py +14 -0
- package/latticeai/core/agent.py +91 -9
- package/latticeai/core/agent_eval.py +107 -0
- package/latticeai/core/file_generation.py +279 -2
- package/latticeai/core/legacy_compatibility.py +1 -1
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/workspace_os.py +1 -1
- package/latticeai/runtime/persistence_runtime.py +19 -1
- package/latticeai/runtime/router_registration.py +8 -1
- package/latticeai/services/app_context.py +3 -0
- package/latticeai/services/architecture_readiness.py +1 -1
- package/latticeai/services/automation_execution.py +266 -0
- package/latticeai/services/automation_intelligence.py +7 -0
- package/latticeai/services/command_center.py +20 -1
- package/latticeai/services/folder_watch.py +391 -0
- package/latticeai/services/funnel_metrics.py +202 -0
- package/latticeai/services/product_readiness.py +1 -1
- package/latticeai/services/search_service.py +17 -1
- package/latticeai/setup/demo_corpus.py +98 -0
- package/latticeai/tools/filesystem.py +38 -1
- package/package.json +1 -1
- package/scripts/bench_models.py +234 -5
- 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 +28 -27
- package/static/app/assets/Act-DmdruVKV.js +1 -0
- package/static/app/assets/{Brain-BnGbytHa.js → Brain-DEY9jLVt.js} +2 -2
- package/static/app/assets/BrainHome-CeNaxjP1.js +2 -0
- package/static/app/assets/BrainSignals-CStjIqYi.js +1 -0
- package/static/app/assets/{Capture-CAKnLcb0.js → Capture-CVZ09QXi.js} +1 -1
- package/static/app/assets/CommandPalette-DepwOQFv.js +1 -0
- package/static/app/assets/Library-Bp0n-HlW.js +1 -0
- package/static/app/assets/{LivingBrain-C3VbogVo.js → LivingBrain-DxP4efJF.js} +1 -1
- package/static/app/assets/{ProductFlow-BLau7c7N.js → ProductFlow-DRbm7NEq.js} +1 -1
- package/static/app/assets/ReviewCard-C4HAO7A3.js +3 -0
- package/static/app/assets/{System-DRHc_6Su.js → System-ByQcmJW-.js} +1 -1
- package/static/app/assets/{bot-DZkDdh-A.js → bot-BNDyZLR7.js} +1 -1
- package/static/app/assets/circle-play-BkhdcHgd.js +1 -0
- package/static/app/assets/{cpu-DyvsDBbj.js → cpu-C6jjYm6i.js} +1 -1
- package/static/app/assets/{folder-open-CffoIr9t.js → folder-open-DjGIvDBQ.js} +1 -1
- package/static/app/assets/{hard-drive-C1oojrHb.js → hard-drive-BlSbwSaT.js} +1 -1
- package/static/app/assets/index-Bge3DXW7.css +2 -0
- package/static/app/assets/{index-BS_3aN1n.js → index-CHu7cgj3.js} +3 -3
- package/static/app/assets/{input-B_U7zvbR.js → input-DVDI0YR3.js} +1 -1
- package/static/app/assets/{navigation-xG-gyjbe.js → navigation-BddhEWA0.js} +1 -1
- package/static/app/assets/{network-BF13HJJF.js → network-pYQt5oBu.js} +1 -1
- package/static/app/assets/{primitives-DsuKh3M8.js → primitives-D7gCdEvS.js} +1 -1
- package/static/app/assets/shield-alert-K9RKGQeg.js +1 -0
- package/static/app/assets/{textarea-DsDL4ET8.js → textarea-sqQmoBKL.js} +1 -1
- package/static/app/assets/useFocusTrap-7EV9dFP2.js +1 -0
- package/static/app/assets/utils-uQYKXNeq.js +7 -0
- package/static/app/index.html +4 -4
- package/static/sw.js +1 -1
- package/static/app/assets/Act-Cx_gZNbE.js +0 -1
- package/static/app/assets/BrainHome-CRUMJL93.js +0 -2
- package/static/app/assets/BrainSignals-BP8-Qh2g.js +0 -1
- package/static/app/assets/CommandPalette-BoUTXBfm.js +0 -1
- package/static/app/assets/Library-PcfVUco6.js +0 -1
- package/static/app/assets/ReviewCard-CX5RqRqx.js +0 -1
- package/static/app/assets/download-C7yxYHvz.js +0 -1
- package/static/app/assets/index-DU78Kogz.css +0 -2
- package/static/app/assets/sunrise-CIP2ELJ1.js +0 -1
- package/static/app/assets/utils-Dh-Dwtkb.js +0 -7
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"""Automation "run now" support: dry-run reports + execution log surfacing.
|
|
2
|
+
|
|
3
|
+
Backlog #6 (review §7.2 F, Wave 2): after installing an automation the user
|
|
4
|
+
should be able to run it once immediately — first as a **dry run** that
|
|
5
|
+
reports exactly what WOULD happen without side effects, then for real — and
|
|
6
|
+
every automation card should show its last execution result.
|
|
7
|
+
|
|
8
|
+
This module owns the deterministic policy pieces:
|
|
9
|
+
|
|
10
|
+
* :func:`dry_run_report` — walks the workflow's node graph (same normalizer
|
|
11
|
+
as the engine) and describes each step without invoking any runner. No
|
|
12
|
+
model call, no store write, no side effects.
|
|
13
|
+
* :func:`build_last_execution` / :func:`summarize_workflow_run` — the compact
|
|
14
|
+
``last_execution`` record persisted on the workflow's metadata and surfaced
|
|
15
|
+
by the automation overview + daily briefing.
|
|
16
|
+
* :func:`last_execution_view` — read-side merge: the stamped metadata record
|
|
17
|
+
vs. the newest persisted workflow run (so trigger-driven runs surface too).
|
|
18
|
+
* :func:`enqueue_failed_execution` — failed live executions become a review
|
|
19
|
+
queue item (source ``workflow_run``, kind ``automation_failure``) so
|
|
20
|
+
failures land in the same inbox as every other automation outcome.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import logging
|
|
26
|
+
from typing import Any, Dict, List, Optional
|
|
27
|
+
|
|
28
|
+
from latticeai.core.timeutil import now_iso as _now
|
|
29
|
+
|
|
30
|
+
LOGGER = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
AUTOMATION_CREATED_FROM = frozenset({
|
|
33
|
+
"automation_suggestion",
|
|
34
|
+
"brain_automation_recipe",
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
_TRIGGER_LABEL = {
|
|
38
|
+
"interval": "wait for the user-enabled schedule",
|
|
39
|
+
"brain_event": "wait for new Brain knowledge",
|
|
40
|
+
"manual": "start when the user asks",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def is_automation_workflow(workflow: Dict[str, Any]) -> bool:
|
|
45
|
+
metadata = (workflow or {}).get("metadata") or {}
|
|
46
|
+
return metadata.get("created_from") in AUTOMATION_CREATED_FROM
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def dry_run_report(workflow: Dict[str, Any]) -> Dict[str, Any]:
|
|
50
|
+
"""Describe what a run WOULD do — deterministically, with no side effects.
|
|
51
|
+
|
|
52
|
+
Walks the normalized node chain (``trigger`` first, then ``next``
|
|
53
|
+
pointers) and emits one human-readable step per node. Nothing is
|
|
54
|
+
executed: no runner, no model, no store write.
|
|
55
|
+
"""
|
|
56
|
+
from lattice_brain.workflow import normalize_definition, validate_definition
|
|
57
|
+
|
|
58
|
+
definition = normalize_definition(workflow or {})
|
|
59
|
+
errors = validate_definition(definition)
|
|
60
|
+
metadata = definition.get("metadata") or {}
|
|
61
|
+
steps: List[Dict[str, Any]] = []
|
|
62
|
+
|
|
63
|
+
nodes = {node.get("id"): node for node in definition.get("nodes") or []}
|
|
64
|
+
current = next(
|
|
65
|
+
(node for node in definition.get("nodes") or [] if node.get("type") == "trigger"),
|
|
66
|
+
None,
|
|
67
|
+
)
|
|
68
|
+
visited: set = set()
|
|
69
|
+
while current is not None and current.get("id") not in visited:
|
|
70
|
+
visited.add(current.get("id"))
|
|
71
|
+
ntype = str(current.get("type") or "")
|
|
72
|
+
config = current.get("config") or {}
|
|
73
|
+
if ntype == "trigger":
|
|
74
|
+
would = _TRIGGER_LABEL.get(
|
|
75
|
+
str(config.get("trigger") or "manual"),
|
|
76
|
+
"start when triggered",
|
|
77
|
+
)
|
|
78
|
+
would = f"skipped in a manual run ({would})"
|
|
79
|
+
elif ntype == "agent":
|
|
80
|
+
prompt = str(config.get("prompt") or config.get("goal") or "")[:160]
|
|
81
|
+
would = f"ask the local agent to draft: {prompt}" if prompt else (
|
|
82
|
+
"ask the local agent to draft a review item"
|
|
83
|
+
)
|
|
84
|
+
elif ntype == "tool":
|
|
85
|
+
would = f"run tool '{config.get('tool') or current.get('name') or 'tool'}'"
|
|
86
|
+
elif ntype == "skill":
|
|
87
|
+
would = f"run skill '{config.get('skill') or current.get('name') or 'skill'}'"
|
|
88
|
+
elif ntype == "plugin":
|
|
89
|
+
would = f"run plugin '{config.get('plugin') or current.get('name') or 'plugin'}'"
|
|
90
|
+
elif ntype == "condition":
|
|
91
|
+
would = "evaluate a branch condition"
|
|
92
|
+
elif ntype == "output":
|
|
93
|
+
would = "deliver the draft to the review inbox"
|
|
94
|
+
else:
|
|
95
|
+
would = f"run node '{current.get('id')}'"
|
|
96
|
+
steps.append({
|
|
97
|
+
"node": current.get("id"),
|
|
98
|
+
"type": ntype,
|
|
99
|
+
"name": current.get("name") or current.get("id"),
|
|
100
|
+
"would": would,
|
|
101
|
+
})
|
|
102
|
+
next_id = current.get("next")
|
|
103
|
+
current = nodes.get(next_id) if next_id else None
|
|
104
|
+
|
|
105
|
+
executable = [s for s in steps if s["type"] not in {"trigger", "output", "condition"}]
|
|
106
|
+
creates = list(metadata.get("creates") or [])
|
|
107
|
+
if errors:
|
|
108
|
+
summary = "invalid workflow: " + "; ".join(errors[:3])
|
|
109
|
+
status = "invalid"
|
|
110
|
+
else:
|
|
111
|
+
summary = (
|
|
112
|
+
f"{len(executable)} step(s) would run locally and produce a reviewable draft"
|
|
113
|
+
)
|
|
114
|
+
if creates:
|
|
115
|
+
summary += " (" + ", ".join(str(c) for c in creates[:3]) + ")"
|
|
116
|
+
summary += "; no external actions, nothing is written until you approve."
|
|
117
|
+
status = "ok"
|
|
118
|
+
return {
|
|
119
|
+
"mode": "dry_run",
|
|
120
|
+
"status": status,
|
|
121
|
+
"steps": steps,
|
|
122
|
+
"summary": summary,
|
|
123
|
+
"side_effects": False,
|
|
124
|
+
"validation_errors": errors,
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def build_last_execution(
|
|
129
|
+
*,
|
|
130
|
+
mode: str,
|
|
131
|
+
status: str,
|
|
132
|
+
summary: str,
|
|
133
|
+
run_id: Optional[str] = None,
|
|
134
|
+
) -> Dict[str, Any]:
|
|
135
|
+
"""The compact per-automation execution record (persisted on metadata)."""
|
|
136
|
+
return {
|
|
137
|
+
"mode": mode, # dry_run | live
|
|
138
|
+
"status": status, # ok | failed | running | partial | ...
|
|
139
|
+
"summary": str(summary or "")[:300],
|
|
140
|
+
"run_id": run_id,
|
|
141
|
+
"finished_at": _now(),
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def summarize_workflow_run(run: Dict[str, Any]) -> str:
|
|
146
|
+
"""One honest line for a persisted workflow run."""
|
|
147
|
+
status = str(run.get("status") or "unknown")
|
|
148
|
+
timeline = run.get("timeline") or []
|
|
149
|
+
if status == "failed":
|
|
150
|
+
detail = ""
|
|
151
|
+
for entry in reversed(timeline):
|
|
152
|
+
if entry.get("status") in {"failed", "error"}:
|
|
153
|
+
detail = str(
|
|
154
|
+
entry.get("reason") or entry.get("detail") or entry.get("errors") or ""
|
|
155
|
+
)[:160]
|
|
156
|
+
break
|
|
157
|
+
if not detail:
|
|
158
|
+
detail = str((run.get("outputs") or {}).get("error") or "")[:160]
|
|
159
|
+
return f"failed after {len(timeline)} step(s)" + (f": {detail}" if detail else "")
|
|
160
|
+
if status == "awaiting_approval":
|
|
161
|
+
return "paused — a step needs your approval before it can run"
|
|
162
|
+
if status in {"queued", "running", "cancelling"}:
|
|
163
|
+
return "still running"
|
|
164
|
+
return f"{status} — {len(timeline)} step(s) recorded"
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def last_execution_view(
|
|
168
|
+
workflow: Dict[str, Any],
|
|
169
|
+
*,
|
|
170
|
+
store: Any = None,
|
|
171
|
+
workspace_id: Optional[str] = None,
|
|
172
|
+
) -> Optional[Dict[str, Any]]:
|
|
173
|
+
"""Read-side ``last_execution``: stamped metadata vs. newest persisted run.
|
|
174
|
+
|
|
175
|
+
Run-now stamps ``metadata.last_execution`` directly; trigger/background
|
|
176
|
+
runs only leave workflow-run rows. Whichever is newer wins so the card is
|
|
177
|
+
honest about the latest execution regardless of how it was started.
|
|
178
|
+
"""
|
|
179
|
+
metadata = (workflow or {}).get("metadata") or {}
|
|
180
|
+
stamped = metadata.get("last_execution")
|
|
181
|
+
stamped = dict(stamped) if isinstance(stamped, dict) else None
|
|
182
|
+
|
|
183
|
+
latest_run: Optional[Dict[str, Any]] = None
|
|
184
|
+
if store is not None and hasattr(store, "list_workflow_runs"):
|
|
185
|
+
try:
|
|
186
|
+
runs = (
|
|
187
|
+
store.list_workflow_runs(
|
|
188
|
+
workflow_id=workflow.get("id"), limit=1, workspace_id=workspace_id
|
|
189
|
+
)
|
|
190
|
+
or {}
|
|
191
|
+
).get("runs") or []
|
|
192
|
+
latest_run = runs[0] if runs else None
|
|
193
|
+
except Exception: # noqa: BLE001 — surfacing must never break the overview
|
|
194
|
+
LOGGER.exception("automation last-execution run read failed")
|
|
195
|
+
latest_run = None
|
|
196
|
+
|
|
197
|
+
derived: Optional[Dict[str, Any]] = None
|
|
198
|
+
if latest_run is not None:
|
|
199
|
+
derived = {
|
|
200
|
+
"mode": "live",
|
|
201
|
+
"status": str(latest_run.get("status") or "unknown"),
|
|
202
|
+
"summary": summarize_workflow_run(latest_run),
|
|
203
|
+
"run_id": latest_run.get("id"),
|
|
204
|
+
"finished_at": str(
|
|
205
|
+
latest_run.get("completed_at")
|
|
206
|
+
or latest_run.get("updated_at")
|
|
207
|
+
or latest_run.get("created_at")
|
|
208
|
+
or ""
|
|
209
|
+
),
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if stamped is None:
|
|
213
|
+
return derived
|
|
214
|
+
if derived is None:
|
|
215
|
+
return stamped
|
|
216
|
+
return derived if str(derived.get("finished_at") or "") > str(
|
|
217
|
+
stamped.get("finished_at") or ""
|
|
218
|
+
) else stamped
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def enqueue_failed_execution(
|
|
222
|
+
review_queue: Any,
|
|
223
|
+
*,
|
|
224
|
+
workflow: Dict[str, Any],
|
|
225
|
+
run_id: Optional[str],
|
|
226
|
+
error: str,
|
|
227
|
+
user_email: Optional[str] = None,
|
|
228
|
+
workspace_id: Optional[str] = None,
|
|
229
|
+
) -> Optional[Dict[str, Any]]:
|
|
230
|
+
"""Failed live execution → review queue item (never raises)."""
|
|
231
|
+
if review_queue is None:
|
|
232
|
+
return None
|
|
233
|
+
name = str(workflow.get("name") or "Automation")
|
|
234
|
+
try:
|
|
235
|
+
return review_queue.create(
|
|
236
|
+
title=f"Automation failed: {name}"[:160],
|
|
237
|
+
summary=str(error or "")[:500],
|
|
238
|
+
source="workflow_run",
|
|
239
|
+
kind="automation_failure",
|
|
240
|
+
payload={
|
|
241
|
+
"workflow_id": workflow.get("id"),
|
|
242
|
+
"run_id": run_id,
|
|
243
|
+
"error": str(error or "")[:500],
|
|
244
|
+
},
|
|
245
|
+
provenance={
|
|
246
|
+
"workflow_id": workflow.get("id"),
|
|
247
|
+
"run_id": run_id,
|
|
248
|
+
"origin": "automation_run_now",
|
|
249
|
+
},
|
|
250
|
+
user_email=user_email,
|
|
251
|
+
workspace_id=workspace_id,
|
|
252
|
+
)
|
|
253
|
+
except Exception: # noqa: BLE001 — review enqueue is best-effort surfacing
|
|
254
|
+
LOGGER.exception("automation failure review enqueue failed")
|
|
255
|
+
return None
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
__all__ = [
|
|
259
|
+
"AUTOMATION_CREATED_FROM",
|
|
260
|
+
"is_automation_workflow",
|
|
261
|
+
"dry_run_report",
|
|
262
|
+
"build_last_execution",
|
|
263
|
+
"summarize_workflow_run",
|
|
264
|
+
"last_execution_view",
|
|
265
|
+
"enqueue_failed_execution",
|
|
266
|
+
]
|
|
@@ -560,6 +560,8 @@ class AutomationIntelligenceService:
|
|
|
560
560
|
except Exception:
|
|
561
561
|
LOGGER.exception("automation intelligence workflow read failed")
|
|
562
562
|
workflows = []
|
|
563
|
+
from latticeai.services.automation_execution import last_execution_view
|
|
564
|
+
|
|
563
565
|
for workflow in workflows:
|
|
564
566
|
metadata = (workflow or {}).get("metadata") or {}
|
|
565
567
|
if metadata.get("created_from") not in {"automation_suggestion", "brain_automation_recipe"}:
|
|
@@ -573,6 +575,11 @@ class AutomationIntelligenceService:
|
|
|
573
575
|
"enabled": metadata.get("automation_state") == "enabled",
|
|
574
576
|
"requires_user_enable": bool(metadata.get("requires_user_enable", True)),
|
|
575
577
|
"creates": metadata.get("creates") or [],
|
|
578
|
+
# Execution log surfacing (backlog #6): the newest of the
|
|
579
|
+
# run-now stamp vs. the latest persisted workflow run.
|
|
580
|
+
"last_execution": last_execution_view(
|
|
581
|
+
workflow, store=self._store, workspace_id=workspace_id
|
|
582
|
+
),
|
|
576
583
|
})
|
|
577
584
|
return {
|
|
578
585
|
"suggestions": suggestion_report["suggestions"],
|
|
@@ -147,18 +147,37 @@ class CommandCenterService:
|
|
|
147
147
|
workflows = self._workflows(workspace_id=workspace_id)
|
|
148
148
|
enabled = 0
|
|
149
149
|
drafts = 0
|
|
150
|
+
last_execution: Optional[Dict[str, Any]] = None
|
|
150
151
|
for workflow in workflows:
|
|
151
152
|
metadata = (workflow or {}).get("metadata") or {}
|
|
152
153
|
if metadata.get("automation_state") == "enabled":
|
|
153
154
|
enabled += 1
|
|
154
155
|
elif metadata.get("automation_state") == "draft_disabled":
|
|
155
156
|
drafts += 1
|
|
156
|
-
|
|
157
|
+
# Execution log surfacing (backlog #6): the briefing carries the
|
|
158
|
+
# most recent stamped automation execution so "did my automation
|
|
159
|
+
# run, and how did it go?" is answered on the home surface.
|
|
160
|
+
stamped = metadata.get("last_execution")
|
|
161
|
+
if isinstance(stamped, dict) and str(stamped.get("finished_at") or "") > str(
|
|
162
|
+
(last_execution or {}).get("finished_at") or ""
|
|
163
|
+
):
|
|
164
|
+
last_execution = {
|
|
165
|
+
"workflow_id": workflow.get("id"),
|
|
166
|
+
"name": _clip(workflow.get("name"), 120),
|
|
167
|
+
"mode": stamped.get("mode"),
|
|
168
|
+
"status": stamped.get("status"),
|
|
169
|
+
"summary": _clip(stamped.get("summary"), 200),
|
|
170
|
+
"finished_at": stamped.get("finished_at"),
|
|
171
|
+
}
|
|
172
|
+
section = {
|
|
157
173
|
"available": True,
|
|
158
174
|
"total": len(workflows),
|
|
159
175
|
"enabled": enabled,
|
|
160
176
|
"drafts": drafts,
|
|
161
177
|
}
|
|
178
|
+
if last_execution is not None:
|
|
179
|
+
section["last_execution"] = last_execution
|
|
180
|
+
return section
|
|
162
181
|
|
|
163
182
|
def _review_section(
|
|
164
183
|
self, *, user_email: Optional[str], workspace_id: Optional[str]
|