solidworks-mcp-python 1.5.0__py3-none-any.whl
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.
- solidworks_mcp/__init__.py +57 -0
- solidworks_mcp/adapters/__init__.py +40 -0
- solidworks_mcp/adapters/base.py +1882 -0
- solidworks_mcp/adapters/circuit_breaker.py +2052 -0
- solidworks_mcp/adapters/com_executor.py +252 -0
- solidworks_mcp/adapters/complexity_analyzer.py +272 -0
- solidworks_mcp/adapters/connection_pool.py +1884 -0
- solidworks_mcp/adapters/factory.py +366 -0
- solidworks_mcp/adapters/intelligent_router.py +218 -0
- solidworks_mcp/adapters/mock_adapter.py +4016 -0
- solidworks_mcp/adapters/pywin32_adapter.py +2842 -0
- solidworks_mcp/adapters/solidworks/__init__.py +17 -0
- solidworks_mcp/adapters/solidworks/bitmap.py +180 -0
- solidworks_mcp/adapters/solidworks/com_utils.py +56 -0
- solidworks_mcp/adapters/solidworks/dimension_layout.py +753 -0
- solidworks_mcp/adapters/solidworks/documentation.py +1476 -0
- solidworks_mcp/adapters/solidworks/drawing_standard.py +1993 -0
- solidworks_mcp/adapters/solidworks/face_selection.py +392 -0
- solidworks_mcp/adapters/solidworks/features.py +3362 -0
- solidworks_mcp/adapters/solidworks/io.py +3649 -0
- solidworks_mcp/adapters/solidworks/rebuild_diagnostics.py +269 -0
- solidworks_mcp/adapters/solidworks/rebuild_inspector.py +146 -0
- solidworks_mcp/adapters/solidworks/selection.py +308 -0
- solidworks_mcp/adapters/solidworks/sketch.py +2545 -0
- solidworks_mcp/adapters/sw_feature_errors.py +66 -0
- solidworks_mcp/adapters/sw_type_info.py +317 -0
- solidworks_mcp/adapters/vba_adapter.py +318 -0
- solidworks_mcp/adapters/vba_macro_executor.py +215 -0
- solidworks_mcp/agents/__init__.py +68 -0
- solidworks_mcp/agents/design_knowledge/assembly_patterns.md +127 -0
- solidworks_mcp/agents/design_knowledge/hinges.md +118 -0
- solidworks_mcp/agents/design_knowledge/snap_fits.md +93 -0
- solidworks_mcp/agents/design_knowledge/solidworks_basics.md +3 -0
- solidworks_mcp/agents/design_knowledge/tolerancing.md +128 -0
- solidworks_mcp/agents/harness.py +216 -0
- solidworks_mcp/agents/history_db.py +1508 -0
- solidworks_mcp/agents/retrieval_index.py +164 -0
- solidworks_mcp/agents/schemas.py +165 -0
- solidworks_mcp/agents/smoke_test.py +285 -0
- solidworks_mcp/agents/soc_exporter.py +650 -0
- solidworks_mcp/agents/soc_pickup.py +332 -0
- solidworks_mcp/agents/soc_rewind.py +221 -0
- solidworks_mcp/agents/vector_rag.py +665 -0
- solidworks_mcp/cache/__init__.py +5 -0
- solidworks_mcp/cache/response_cache.py +164 -0
- solidworks_mcp/config.py +729 -0
- solidworks_mcp/exceptions.py +37 -0
- solidworks_mcp/security/__init__.py +68 -0
- solidworks_mcp/security/auth.py +128 -0
- solidworks_mcp/security/cors.py +27 -0
- solidworks_mcp/security/rate_limiting.py +90 -0
- solidworks_mcp/security/runtime.py +155 -0
- solidworks_mcp/server.py +777 -0
- solidworks_mcp/server_cli_fixed.py +120 -0
- solidworks_mcp/setup/__init__.py +1 -0
- solidworks_mcp/setup/cli.py +399 -0
- solidworks_mcp/setup/clients.py +157 -0
- solidworks_mcp/setup/config_file.py +230 -0
- solidworks_mcp/setup/handshake.py +202 -0
- solidworks_mcp/setup/launcher.py +102 -0
- solidworks_mcp/setup/legacy.py +100 -0
- solidworks_mcp/setup/registration.py +193 -0
- solidworks_mcp/setup/solidworks.py +49 -0
- solidworks_mcp/tools/__init__.py +64 -0
- solidworks_mcp/tools/analysis.py +723 -0
- solidworks_mcp/tools/automation.py +578 -0
- solidworks_mcp/tools/docs_discovery.py +1476 -0
- solidworks_mcp/tools/drawing.py +1015 -0
- solidworks_mcp/tools/drawing_analysis.py +456 -0
- solidworks_mcp/tools/drawing_automation.py +627 -0
- solidworks_mcp/tools/engineering_data.py +466 -0
- solidworks_mcp/tools/error_classifier.py +232 -0
- solidworks_mcp/tools/export.py +833 -0
- solidworks_mcp/tools/file_facts.py +40 -0
- solidworks_mcp/tools/file_management.py +1473 -0
- solidworks_mcp/tools/input_compat.py +17 -0
- solidworks_mcp/tools/macro_recording.py +464 -0
- solidworks_mcp/tools/modeling.py +2360 -0
- solidworks_mcp/tools/sketching.py +1422 -0
- solidworks_mcp/tools/template_management.py +367 -0
- solidworks_mcp/tools/tool_boundary.py +280 -0
- solidworks_mcp/tools/vba_generation.py +1145 -0
- solidworks_mcp/ui/__init__.py +9 -0
- solidworks_mcp/ui/local_llm.py +725 -0
- solidworks_mcp/ui/routers/__init__.py +10 -0
- solidworks_mcp/ui/routers/checkpoint.py +24 -0
- solidworks_mcp/ui/routers/docs.py +47 -0
- solidworks_mcp/ui/routers/llm.py +83 -0
- solidworks_mcp/ui/routers/local_model.py +69 -0
- solidworks_mcp/ui/routers/model.py +75 -0
- solidworks_mcp/ui/routers/preview.py +38 -0
- solidworks_mcp/ui/routers/session.py +161 -0
- solidworks_mcp/ui/routers/viewer.py +154 -0
- solidworks_mcp/ui/schemas.py +502 -0
- solidworks_mcp/ui/server.py +298 -0
- solidworks_mcp/ui/service.py +481 -0
- solidworks_mcp/ui/services/__init__.py +136 -0
- solidworks_mcp/ui/services/_utils.py +958 -0
- solidworks_mcp/ui/services/checkpoint_service.py +1110 -0
- solidworks_mcp/ui/services/docs_service.py +282 -0
- solidworks_mcp/ui/services/llm_service.py +956 -0
- solidworks_mcp/ui/services/model_service.py +459 -0
- solidworks_mcp/ui/services/parameter_repair_service.py +314 -0
- solidworks_mcp/ui/services/preview_service.py +489 -0
- solidworks_mcp/ui/services/session_service.py +1389 -0
- solidworks_mcp/utils/__init__.py +9 -0
- solidworks_mcp/utils/feature_tree_classifier.py +342 -0
- solidworks_mcp/utils/logging.py +76 -0
- solidworks_mcp/utils/validation.py +77 -0
- solidworks_mcp/version.py +8 -0
- solidworks_mcp_python-1.5.0.dist-info/METADATA +410 -0
- solidworks_mcp_python-1.5.0.dist-info/RECORD +115 -0
- solidworks_mcp_python-1.5.0.dist-info/WHEEL +4 -0
- solidworks_mcp_python-1.5.0.dist-info/entry_points.txt +3 -0
- solidworks_mcp_python-1.5.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,1389 @@
|
|
|
1
|
+
"""Session management and dashboard state assembly for the Prefab CAD dashboard.
|
|
2
|
+
|
|
3
|
+
Responsibilities (Single Responsibility principle):
|
|
4
|
+
- Create and hydrate dashboard session rows.
|
|
5
|
+
- Persist user decisions (brief approval, family acceptance, workflow selection,
|
|
6
|
+
preferences, notes, context snapshots).
|
|
7
|
+
- Assemble the ``DashboardUIState`` payload consumed by the Prefab UI renderer.
|
|
8
|
+
|
|
9
|
+
Does NOT own: adapter calls, LLM calls, preview export, RAG ingestion.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import time
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from loguru import logger
|
|
20
|
+
|
|
21
|
+
from ...agents.history_db import (
|
|
22
|
+
get_design_session,
|
|
23
|
+
insert_plan_checkpoint,
|
|
24
|
+
list_evidence_links,
|
|
25
|
+
list_model_state_snapshots,
|
|
26
|
+
list_plan_checkpoints,
|
|
27
|
+
list_tool_call_records,
|
|
28
|
+
update_plan_checkpoint,
|
|
29
|
+
upsert_design_session,
|
|
30
|
+
)
|
|
31
|
+
from ...config import load_config
|
|
32
|
+
from ..schemas import DashboardCheckpoint, DashboardEvidenceRow, DashboardUIState
|
|
33
|
+
from ._utils import (
|
|
34
|
+
DEFAULT_API_ORIGIN,
|
|
35
|
+
DEFAULT_PREVIEW_ORIENTATION,
|
|
36
|
+
DEFAULT_SESSION_ID,
|
|
37
|
+
DEFAULT_SOURCE_MODE,
|
|
38
|
+
DEFAULT_USER_GOAL,
|
|
39
|
+
DEFAULT_WORKFLOW_MODE,
|
|
40
|
+
context_file_path,
|
|
41
|
+
ensure_preview_dir,
|
|
42
|
+
feature_grounding_warning_text,
|
|
43
|
+
merge_metadata,
|
|
44
|
+
normalize_model_name_for_provider,
|
|
45
|
+
normalize_workflow_mode,
|
|
46
|
+
parse_json_blob,
|
|
47
|
+
persist_ui_action,
|
|
48
|
+
provider_from_model_name,
|
|
49
|
+
provider_has_credentials,
|
|
50
|
+
safe_context_name,
|
|
51
|
+
sanitize_model_path_text,
|
|
52
|
+
sanitize_preview_viewer_url,
|
|
53
|
+
sanitize_ui_text,
|
|
54
|
+
trace_json,
|
|
55
|
+
trace_session_row,
|
|
56
|
+
trace_tool_records,
|
|
57
|
+
workflow_copy,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
# Session bootstrap
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
_DEFAULT_CHECKPOINT_SPECS: list[dict[str, Any]] = [
|
|
65
|
+
{
|
|
66
|
+
"title": "Base profile",
|
|
67
|
+
"goal": "Create the base sketch profile",
|
|
68
|
+
"tools": ["create_sketch", "add_line"],
|
|
69
|
+
"rationale": "Establish the primary profile before any 3D feature.",
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"title": "Extrude body",
|
|
73
|
+
"goal": "Create the main body",
|
|
74
|
+
"tools": ["create_extrusion"],
|
|
75
|
+
"rationale": "Turn the approved sketch into the primary solid.",
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"title": "Hole pattern",
|
|
79
|
+
"goal": "Add fastener holes and cable clearance",
|
|
80
|
+
"tools": ["create_sketch", "create_cut"],
|
|
81
|
+
"rationale": "Apply mounting features after the body dimensions are stable.",
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"title": "Clearance verify",
|
|
85
|
+
"goal": "Check fit and interference",
|
|
86
|
+
"tools": ["check_interference"],
|
|
87
|
+
"rationale": "Validate the assembly path before release or print export.",
|
|
88
|
+
},
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
_UJOINT_CHECKPOINT_SPECS: list[dict[str, Any]] = [
|
|
92
|
+
{
|
|
93
|
+
"title": "Reference mode selection",
|
|
94
|
+
"goal": "Choose target mode (reference reproduction or print variant) and inspect constraints",
|
|
95
|
+
"tools": [
|
|
96
|
+
"open_model",
|
|
97
|
+
"list_features",
|
|
98
|
+
"create_sketch",
|
|
99
|
+
"create_extrusion",
|
|
100
|
+
"create_cut",
|
|
101
|
+
],
|
|
102
|
+
"rationale": "Locks feature-order and dimension guardrails before generating the rest of the set.",
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"title": "Part set planning",
|
|
106
|
+
"goal": "Generate the remaining U-joint part plans (yokes, spider, pin, crank family)",
|
|
107
|
+
"tools": [
|
|
108
|
+
"classify_feature_tree",
|
|
109
|
+
"create_part",
|
|
110
|
+
"create_sketch",
|
|
111
|
+
"create_extrusion",
|
|
112
|
+
"create_revolve",
|
|
113
|
+
],
|
|
114
|
+
"rationale": "Decomposes U-joint into deterministic part-level steps with print-aware constraints.",
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
"title": "Assembly build",
|
|
118
|
+
"goal": "Create UJoint.SLDASM and add deterministic mates with interference checks",
|
|
119
|
+
"tools": ["create_assembly", "check_interference"],
|
|
120
|
+
"rationale": "Ensures mate strategy and collision checks are covered before QA.",
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
"title": "Final quality checks",
|
|
124
|
+
"goal": "Run final pass/fail checks and export preview images",
|
|
125
|
+
"tools": ["list_features", "get_model_info", "export_image"],
|
|
126
|
+
"rationale": "Confirms required feature trees and assembly readiness for release.",
|
|
127
|
+
},
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _checkpoint_specs_for_goal(user_goal: str) -> list[dict[str, Any]]:
|
|
132
|
+
"""Select initial checkpoint template by goal text.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
user_goal: Goal text provided by the user.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
List of checkpoint specification dicts.
|
|
139
|
+
"""
|
|
140
|
+
goal = (user_goal or "").strip().lower()
|
|
141
|
+
if "u-joint" in goal or "ujoint" in goal:
|
|
142
|
+
return list(_UJOINT_CHECKPOINT_SPECS)
|
|
143
|
+
return list(_DEFAULT_CHECKPOINT_SPECS)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def default_checkpoint_specs() -> list[dict[str, Any]]:
|
|
147
|
+
"""Return the four default checkpoint specs used when no LLM plan exists.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
List of checkpoint specification dicts.
|
|
151
|
+
"""
|
|
152
|
+
return list(_DEFAULT_CHECKPOINT_SPECS)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def ensure_dashboard_session(
|
|
156
|
+
session_id: str = DEFAULT_SESSION_ID,
|
|
157
|
+
*,
|
|
158
|
+
user_goal: str | None = None,
|
|
159
|
+
db_path: Path | None = None,
|
|
160
|
+
) -> dict[str, Any]:
|
|
161
|
+
"""Ensure one dashboard session row and default checkpoints exist.
|
|
162
|
+
|
|
163
|
+
Creates the session row on first call and inserts default checkpoints when
|
|
164
|
+
none are present yet. On subsequent calls it updates the ``user_goal`` column
|
|
165
|
+
when the supplied goal differs from the stored one.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
session_id: Dashboard session identifier.
|
|
169
|
+
user_goal: Optional initial or updated design goal.
|
|
170
|
+
db_path: Optional override for the SQLite database path.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
The current session row dict.
|
|
174
|
+
"""
|
|
175
|
+
session_row = get_design_session(session_id, db_path=db_path)
|
|
176
|
+
requested_goal = sanitize_ui_text(user_goal, "") if user_goal is not None else ""
|
|
177
|
+
if session_row is None:
|
|
178
|
+
upsert_design_session(
|
|
179
|
+
session_id=session_id,
|
|
180
|
+
user_goal=requested_goal or DEFAULT_USER_GOAL,
|
|
181
|
+
source_mode=DEFAULT_SOURCE_MODE,
|
|
182
|
+
status="inspect",
|
|
183
|
+
metadata_json=json.dumps(
|
|
184
|
+
{
|
|
185
|
+
"normalized_brief": requested_goal or DEFAULT_USER_GOAL,
|
|
186
|
+
"preview_orientation": DEFAULT_PREVIEW_ORIENTATION,
|
|
187
|
+
},
|
|
188
|
+
ensure_ascii=True,
|
|
189
|
+
),
|
|
190
|
+
db_path=db_path,
|
|
191
|
+
)
|
|
192
|
+
elif requested_goal and requested_goal != session_row["user_goal"]:
|
|
193
|
+
upsert_design_session(
|
|
194
|
+
session_id=session_id,
|
|
195
|
+
user_goal=requested_goal,
|
|
196
|
+
source_mode=session_row["source_mode"],
|
|
197
|
+
accepted_family=session_row["accepted_family"],
|
|
198
|
+
status=session_row["status"],
|
|
199
|
+
current_checkpoint_index=session_row["current_checkpoint_index"],
|
|
200
|
+
metadata_json=session_row["metadata_json"],
|
|
201
|
+
db_path=db_path,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
checkpoints = list_plan_checkpoints(session_id, db_path=db_path)
|
|
205
|
+
if not checkpoints:
|
|
206
|
+
base_goal = (
|
|
207
|
+
requested_goal or session_row.get("user_goal") if session_row else ""
|
|
208
|
+
)
|
|
209
|
+
for index, spec in enumerate(
|
|
210
|
+
_checkpoint_specs_for_goal(str(base_goal)), start=1
|
|
211
|
+
):
|
|
212
|
+
insert_plan_checkpoint(
|
|
213
|
+
session_id=session_id,
|
|
214
|
+
checkpoint_index=index,
|
|
215
|
+
title=spec["title"],
|
|
216
|
+
planned_action_json=json.dumps(spec, ensure_ascii=True),
|
|
217
|
+
approved_by_user=index == 1,
|
|
218
|
+
db_path=db_path,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
return get_design_session(session_id, db_path=db_path) or {}
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
# ---------------------------------------------------------------------------
|
|
225
|
+
# User-facing state mutation functions
|
|
226
|
+
# ---------------------------------------------------------------------------
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def approve_design_brief(
|
|
230
|
+
session_id: str,
|
|
231
|
+
user_goal: str,
|
|
232
|
+
*,
|
|
233
|
+
db_path: Path | None = None,
|
|
234
|
+
) -> dict[str, Any]:
|
|
235
|
+
"""Persist the accepted design goal and return updated dashboard state.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
session_id: Dashboard session identifier.
|
|
239
|
+
user_goal: Design goal text approved by the user.
|
|
240
|
+
db_path: Optional override for the SQLite database path.
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
Full dashboard state payload.
|
|
244
|
+
"""
|
|
245
|
+
ensure_dashboard_session(session_id, user_goal=user_goal, db_path=db_path)
|
|
246
|
+
persist_ui_action(
|
|
247
|
+
session_id,
|
|
248
|
+
tool_name="ui.approve_brief",
|
|
249
|
+
db_path=db_path,
|
|
250
|
+
user_goal=user_goal,
|
|
251
|
+
metadata_updates={
|
|
252
|
+
"normalized_brief": user_goal,
|
|
253
|
+
"latest_message": "Brief accepted.",
|
|
254
|
+
"latest_error_text": "",
|
|
255
|
+
"remediation_hint": "",
|
|
256
|
+
},
|
|
257
|
+
input_payload={"user_goal": user_goal},
|
|
258
|
+
output_metadata=True,
|
|
259
|
+
)
|
|
260
|
+
return build_dashboard_state(session_id, db_path=db_path)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def accept_family_choice(
|
|
264
|
+
session_id: str,
|
|
265
|
+
family: str | None = None,
|
|
266
|
+
*,
|
|
267
|
+
db_path: Path | None = None,
|
|
268
|
+
) -> dict[str, Any]:
|
|
269
|
+
"""Accept the proposed family classification and advance session status.
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
session_id: Dashboard session identifier.
|
|
273
|
+
family: Family name to accept; falls back to ``proposed_family`` in metadata.
|
|
274
|
+
db_path: Optional override for the SQLite database path.
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
Full dashboard state payload.
|
|
278
|
+
"""
|
|
279
|
+
session_row = ensure_dashboard_session(session_id, db_path=db_path)
|
|
280
|
+
metadata = parse_json_blob(session_row.get("metadata_json"))
|
|
281
|
+
accepted_family = family or metadata.get("proposed_family") or "unknown"
|
|
282
|
+
upsert_design_session(
|
|
283
|
+
session_id=session_id,
|
|
284
|
+
user_goal=session_row.get("user_goal") or DEFAULT_USER_GOAL,
|
|
285
|
+
source_mode=session_row.get("source_mode") or DEFAULT_SOURCE_MODE,
|
|
286
|
+
accepted_family=accepted_family,
|
|
287
|
+
status="planned",
|
|
288
|
+
current_checkpoint_index=session_row.get("current_checkpoint_index") or 0,
|
|
289
|
+
metadata_json=session_row.get("metadata_json"),
|
|
290
|
+
db_path=db_path,
|
|
291
|
+
)
|
|
292
|
+
persist_ui_action(
|
|
293
|
+
session_id,
|
|
294
|
+
tool_name="ui.accept_family",
|
|
295
|
+
db_path=db_path,
|
|
296
|
+
metadata_updates={
|
|
297
|
+
"accepted_family": accepted_family,
|
|
298
|
+
"latest_message": f"Family accepted: {accepted_family}.",
|
|
299
|
+
"latest_error_text": "",
|
|
300
|
+
"remediation_hint": "",
|
|
301
|
+
},
|
|
302
|
+
input_payload={"family": accepted_family},
|
|
303
|
+
)
|
|
304
|
+
return build_dashboard_state(session_id, db_path=db_path)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def reconcile_manual_edits(
|
|
308
|
+
session_id: str,
|
|
309
|
+
*,
|
|
310
|
+
db_path: Path | None = None,
|
|
311
|
+
) -> dict[str, Any]:
|
|
312
|
+
"""Compare the two most-recent snapshots and summarise any detected changes.
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
session_id: Dashboard session identifier.
|
|
316
|
+
db_path: Optional override for the SQLite database path.
|
|
317
|
+
|
|
318
|
+
Returns:
|
|
319
|
+
Full dashboard state payload.
|
|
320
|
+
"""
|
|
321
|
+
ensure_dashboard_session(session_id, db_path=db_path)
|
|
322
|
+
snapshots = list_model_state_snapshots(session_id, db_path=db_path)
|
|
323
|
+
|
|
324
|
+
if len(snapshots) < 2:
|
|
325
|
+
message = (
|
|
326
|
+
"Not enough snapshots yet. Capture another preview after manual edits."
|
|
327
|
+
)
|
|
328
|
+
else:
|
|
329
|
+
latest = snapshots[0]
|
|
330
|
+
previous = snapshots[1]
|
|
331
|
+
changed = latest.get("state_fingerprint") != previous.get(
|
|
332
|
+
"state_fingerprint"
|
|
333
|
+
) or latest.get("screenshot_path") != previous.get("screenshot_path")
|
|
334
|
+
message = (
|
|
335
|
+
"Detected manual changes. Options: accept edits, patch toward goal, or rollback."
|
|
336
|
+
if changed
|
|
337
|
+
else "No visual/state change detected since the last accepted snapshot."
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
persist_ui_action(
|
|
341
|
+
session_id,
|
|
342
|
+
tool_name="ui.reconcile_manual_edits",
|
|
343
|
+
db_path=db_path,
|
|
344
|
+
metadata_updates={"latest_message": message},
|
|
345
|
+
output_payload={"message": message},
|
|
346
|
+
)
|
|
347
|
+
return build_dashboard_state(session_id, db_path=db_path)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def update_ui_preferences(
|
|
351
|
+
session_id: str,
|
|
352
|
+
*,
|
|
353
|
+
assumptions_text: str | None = None,
|
|
354
|
+
model_provider: str | None = None,
|
|
355
|
+
model_profile: str | None = None,
|
|
356
|
+
model_name: str | None = None,
|
|
357
|
+
local_endpoint: str | None = None,
|
|
358
|
+
db_path: Path | None = None,
|
|
359
|
+
) -> dict[str, Any]:
|
|
360
|
+
"""Persist manufacturing assumptions and LLM provider preferences.
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
session_id: Dashboard session identifier.
|
|
364
|
+
assumptions_text: Free-form manufacturing assumption text.
|
|
365
|
+
model_provider: LLM provider identifier (``"github"``, ``"openai"``, etc.).
|
|
366
|
+
model_profile: Capability tier (``"small"``, ``"balanced"``, ``"large"``).
|
|
367
|
+
model_name: Explicit provider-qualified model name override.
|
|
368
|
+
local_endpoint: Ollama / local API base URL.
|
|
369
|
+
db_path: Optional override for the SQLite database path.
|
|
370
|
+
|
|
371
|
+
Returns:
|
|
372
|
+
Full dashboard state payload.
|
|
373
|
+
"""
|
|
374
|
+
import os # local import to keep module-level deps minimal
|
|
375
|
+
|
|
376
|
+
ensure_dashboard_session(session_id, db_path=db_path)
|
|
377
|
+
provider = (model_provider or "github").strip().lower()
|
|
378
|
+
profile = (model_profile or "balanced").strip().lower()
|
|
379
|
+
resolved_model = normalize_model_name_for_provider(
|
|
380
|
+
model_name,
|
|
381
|
+
provider=provider,
|
|
382
|
+
profile=profile,
|
|
383
|
+
)
|
|
384
|
+
resolved_endpoint = sanitize_ui_text(
|
|
385
|
+
local_endpoint,
|
|
386
|
+
os.getenv("SOLIDWORKS_UI_LOCAL_ENDPOINT", "http://127.0.0.1:11434/v1"),
|
|
387
|
+
)
|
|
388
|
+
persist_ui_action(
|
|
389
|
+
session_id,
|
|
390
|
+
tool_name="ui.update_preferences",
|
|
391
|
+
db_path=db_path,
|
|
392
|
+
metadata_updates={
|
|
393
|
+
"assumptions_text": sanitize_ui_text(
|
|
394
|
+
assumptions_text,
|
|
395
|
+
"No assumptions provided yet.",
|
|
396
|
+
),
|
|
397
|
+
"model_provider": provider,
|
|
398
|
+
"model_profile": profile,
|
|
399
|
+
"model_name": resolved_model,
|
|
400
|
+
"local_endpoint": resolved_endpoint,
|
|
401
|
+
"latest_message": "Updated assumptions and model preferences.",
|
|
402
|
+
"latest_error_text": "",
|
|
403
|
+
"remediation_hint": "",
|
|
404
|
+
},
|
|
405
|
+
input_payload={
|
|
406
|
+
"assumptions_text": assumptions_text,
|
|
407
|
+
"model_provider": provider,
|
|
408
|
+
"model_profile": profile,
|
|
409
|
+
"model_name": resolved_model,
|
|
410
|
+
"local_endpoint": resolved_endpoint,
|
|
411
|
+
},
|
|
412
|
+
output_metadata=True,
|
|
413
|
+
)
|
|
414
|
+
return build_dashboard_state(session_id, db_path=db_path)
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def select_workflow_mode(
|
|
418
|
+
session_id: str,
|
|
419
|
+
*,
|
|
420
|
+
workflow_mode: str,
|
|
421
|
+
db_path: Path | None = None,
|
|
422
|
+
) -> dict[str, Any]:
|
|
423
|
+
"""Persist the onboarding workflow branch and reset new-design state when switching.
|
|
424
|
+
|
|
425
|
+
Selecting ``"new_design"`` resets all model-attachment, preview, and clarification
|
|
426
|
+
state so the user starts with a blank slate for the new part.
|
|
427
|
+
|
|
428
|
+
Args:
|
|
429
|
+
session_id: Dashboard session identifier.
|
|
430
|
+
workflow_mode: Target mode (``"edit_existing"`` or ``"new_design"``).
|
|
431
|
+
db_path: Optional override for the SQLite database path.
|
|
432
|
+
|
|
433
|
+
Returns:
|
|
434
|
+
Full dashboard state payload.
|
|
435
|
+
"""
|
|
436
|
+
session_row = ensure_dashboard_session(session_id, db_path=db_path)
|
|
437
|
+
normalized_mode = normalize_workflow_mode(workflow_mode)
|
|
438
|
+
workflow_label, workflow_guidance, _ = workflow_copy(normalized_mode)
|
|
439
|
+
|
|
440
|
+
if normalized_mode == "new_design":
|
|
441
|
+
metadata = parse_json_blob(session_row.get("metadata_json"))
|
|
442
|
+
metadata.update(
|
|
443
|
+
{
|
|
444
|
+
"workflow_mode": normalized_mode,
|
|
445
|
+
"active_model_path": "",
|
|
446
|
+
"active_model_status": "No active model connected yet.",
|
|
447
|
+
"active_model_type": "",
|
|
448
|
+
"active_model_configuration": "",
|
|
449
|
+
"feature_target_text": "",
|
|
450
|
+
"feature_target_status": "No grounded feature target selected.",
|
|
451
|
+
"selected_feature_name": "",
|
|
452
|
+
"selected_feature_selector_name": "",
|
|
453
|
+
"preview_viewer_url": "",
|
|
454
|
+
"preview_view_urls": {},
|
|
455
|
+
"preview_status": "No preview captured yet.",
|
|
456
|
+
"preview_stl_ready": False,
|
|
457
|
+
"preview_png_ready": False,
|
|
458
|
+
"clarifying_questions": [],
|
|
459
|
+
"user_clarification_answer": "",
|
|
460
|
+
"proposed_family": "unclassified",
|
|
461
|
+
"family_confidence": "pending",
|
|
462
|
+
"family_evidence": [],
|
|
463
|
+
"family_warnings": [],
|
|
464
|
+
"mocked_tools": [],
|
|
465
|
+
"rag_source_path": "",
|
|
466
|
+
"rag_status": "No retrieval source ingested yet.",
|
|
467
|
+
"rag_index_path": "",
|
|
468
|
+
"rag_chunk_count": 0,
|
|
469
|
+
"rag_provenance_text": "No retrieval provenance available yet.",
|
|
470
|
+
"docs_context_text": "No docs context loaded yet.",
|
|
471
|
+
"notes_text": "",
|
|
472
|
+
"orchestration_status": "Ready.",
|
|
473
|
+
"context_save_status": "",
|
|
474
|
+
"context_load_status": "",
|
|
475
|
+
"latest_message": f"Workflow selected: {workflow_label}.",
|
|
476
|
+
"latest_error_text": "",
|
|
477
|
+
"remediation_hint": "",
|
|
478
|
+
"normalized_brief": "Describe the new part you want to design.",
|
|
479
|
+
}
|
|
480
|
+
)
|
|
481
|
+
upsert_design_session(
|
|
482
|
+
session_id=session_id,
|
|
483
|
+
user_goal="Describe the new part you want to design.",
|
|
484
|
+
source_mode=session_row.get("source_mode") or DEFAULT_SOURCE_MODE,
|
|
485
|
+
accepted_family=None,
|
|
486
|
+
status="inspect",
|
|
487
|
+
current_checkpoint_index=0,
|
|
488
|
+
metadata_json=json.dumps(metadata, ensure_ascii=True),
|
|
489
|
+
db_path=db_path,
|
|
490
|
+
)
|
|
491
|
+
# Reset all checkpoints so Execute Next starts clean for the new design.
|
|
492
|
+
for row in list_plan_checkpoints(session_id, db_path=db_path):
|
|
493
|
+
update_plan_checkpoint(
|
|
494
|
+
int(row.get("id") or 0),
|
|
495
|
+
approved_by_user=False,
|
|
496
|
+
executed=False,
|
|
497
|
+
result_json="",
|
|
498
|
+
db_path=db_path,
|
|
499
|
+
)
|
|
500
|
+
else:
|
|
501
|
+
metadata = merge_metadata(
|
|
502
|
+
session_id,
|
|
503
|
+
db_path=db_path,
|
|
504
|
+
workflow_mode=normalized_mode,
|
|
505
|
+
latest_message=f"Workflow selected: {workflow_label}.",
|
|
506
|
+
latest_error_text="",
|
|
507
|
+
remediation_hint="",
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
persist_ui_action(
|
|
511
|
+
session_id,
|
|
512
|
+
tool_name="ui.select_workflow_mode",
|
|
513
|
+
db_path=db_path,
|
|
514
|
+
input_payload={"workflow_mode": normalized_mode},
|
|
515
|
+
output_payload={
|
|
516
|
+
"workflow_mode": normalized_mode,
|
|
517
|
+
"workflow_label": workflow_label,
|
|
518
|
+
"workflow_guidance_text": workflow_guidance,
|
|
519
|
+
"metadata": metadata,
|
|
520
|
+
},
|
|
521
|
+
)
|
|
522
|
+
return build_dashboard_state(session_id, db_path=db_path)
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def update_session_notes(
|
|
526
|
+
session_id: str,
|
|
527
|
+
*,
|
|
528
|
+
notes_text: str,
|
|
529
|
+
db_path: Path | None = None,
|
|
530
|
+
api_origin: str = DEFAULT_API_ORIGIN,
|
|
531
|
+
) -> dict[str, Any]:
|
|
532
|
+
"""Persist free-form engineering notes in session metadata.
|
|
533
|
+
|
|
534
|
+
Args:
|
|
535
|
+
session_id: Dashboard session identifier.
|
|
536
|
+
notes_text: Free-text engineering notes content.
|
|
537
|
+
db_path: Optional override for the SQLite database path.
|
|
538
|
+
api_origin: API origin used for URL generation in the returned state.
|
|
539
|
+
|
|
540
|
+
Returns:
|
|
541
|
+
Full dashboard state payload.
|
|
542
|
+
"""
|
|
543
|
+
persist_ui_action(
|
|
544
|
+
session_id,
|
|
545
|
+
tool_name="ui.notes.update",
|
|
546
|
+
db_path=db_path,
|
|
547
|
+
metadata_updates={
|
|
548
|
+
"notes_text": notes_text,
|
|
549
|
+
"latest_message": "Notes saved.",
|
|
550
|
+
"latest_error_text": "",
|
|
551
|
+
"remediation_hint": "",
|
|
552
|
+
},
|
|
553
|
+
input_payload={"notes_text": notes_text},
|
|
554
|
+
)
|
|
555
|
+
return build_dashboard_state(session_id, db_path=db_path, api_origin=api_origin)
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def save_session_context(
|
|
559
|
+
session_id: str,
|
|
560
|
+
*,
|
|
561
|
+
context_name: str | None = None,
|
|
562
|
+
db_path: Path | None = None,
|
|
563
|
+
context_dir: Path | None = None,
|
|
564
|
+
api_origin: str = DEFAULT_API_ORIGIN,
|
|
565
|
+
) -> dict[str, Any]:
|
|
566
|
+
"""Persist the current dashboard state to a plain JSON snapshot file.
|
|
567
|
+
|
|
568
|
+
The snapshot can later be loaded with :func:`load_session_context` to restore
|
|
569
|
+
the same session state across restarts or share it between machines.
|
|
570
|
+
|
|
571
|
+
Args:
|
|
572
|
+
session_id: Dashboard session identifier.
|
|
573
|
+
context_name: Optional human-readable name for the snapshot file.
|
|
574
|
+
db_path: Optional override for the SQLite database path.
|
|
575
|
+
context_dir: Override directory for context snapshot files.
|
|
576
|
+
api_origin: API origin used for URL generation.
|
|
577
|
+
|
|
578
|
+
Returns:
|
|
579
|
+
Full dashboard state payload.
|
|
580
|
+
"""
|
|
581
|
+
state = build_dashboard_state(session_id, db_path=db_path, api_origin=api_origin)
|
|
582
|
+
target_path = context_file_path(
|
|
583
|
+
session_id,
|
|
584
|
+
context_name=context_name,
|
|
585
|
+
context_dir=context_dir,
|
|
586
|
+
)
|
|
587
|
+
payload = {
|
|
588
|
+
"session_id": session_id,
|
|
589
|
+
"saved_at": int(time.time()),
|
|
590
|
+
"state": state,
|
|
591
|
+
}
|
|
592
|
+
target_path.write_text(
|
|
593
|
+
json.dumps(payload, ensure_ascii=True, indent=2), encoding="utf-8"
|
|
594
|
+
)
|
|
595
|
+
message = f"Context saved to {target_path}."
|
|
596
|
+
persist_ui_action(
|
|
597
|
+
session_id,
|
|
598
|
+
tool_name="ui.context.save",
|
|
599
|
+
db_path=db_path,
|
|
600
|
+
metadata_updates={
|
|
601
|
+
"context_save_status": message,
|
|
602
|
+
"context_name_input": safe_context_name(context_name, session_id),
|
|
603
|
+
"context_file_input": str(target_path),
|
|
604
|
+
"last_context_file": str(target_path),
|
|
605
|
+
"latest_message": message,
|
|
606
|
+
"latest_error_text": "",
|
|
607
|
+
"remediation_hint": "",
|
|
608
|
+
},
|
|
609
|
+
input_payload={"context_name": context_name},
|
|
610
|
+
output_payload={"path": str(target_path)},
|
|
611
|
+
)
|
|
612
|
+
return build_dashboard_state(session_id, db_path=db_path, api_origin=api_origin)
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
def load_session_context(
|
|
616
|
+
session_id: str,
|
|
617
|
+
*,
|
|
618
|
+
context_file: str | None = None,
|
|
619
|
+
db_path: Path | None = None,
|
|
620
|
+
context_dir: Path | None = None,
|
|
621
|
+
api_origin: str = DEFAULT_API_ORIGIN,
|
|
622
|
+
) -> dict[str, Any]:
|
|
623
|
+
"""Load a previously saved context snapshot back into session metadata.
|
|
624
|
+
|
|
625
|
+
Selectively merges the ``workflow_mode``, ``assumptions_text``, model path,
|
|
626
|
+
feature targets, provider settings, notes, and docs query from the snapshot.
|
|
627
|
+
Does not overwrite live execution state or checkpoint results.
|
|
628
|
+
|
|
629
|
+
Args:
|
|
630
|
+
session_id: Dashboard session identifier.
|
|
631
|
+
context_file: Path to the JSON snapshot file; defaults to the canonical location.
|
|
632
|
+
db_path: Optional override for the SQLite database path.
|
|
633
|
+
context_dir: Override directory for context snapshot files.
|
|
634
|
+
api_origin: API origin used for URL generation.
|
|
635
|
+
|
|
636
|
+
Returns:
|
|
637
|
+
Full dashboard state payload.
|
|
638
|
+
"""
|
|
639
|
+
context_file_text = sanitize_ui_text(context_file, "")
|
|
640
|
+
source_path = (
|
|
641
|
+
Path(context_file_text)
|
|
642
|
+
if context_file_text
|
|
643
|
+
else context_file_path(session_id, context_dir=context_dir)
|
|
644
|
+
)
|
|
645
|
+
if not source_path.exists():
|
|
646
|
+
message = f"Context load failed. File not found: {source_path}."
|
|
647
|
+
merge_metadata(
|
|
648
|
+
session_id,
|
|
649
|
+
db_path=db_path,
|
|
650
|
+
context_load_status=message,
|
|
651
|
+
context_file_input=str(source_path),
|
|
652
|
+
latest_error_text=message,
|
|
653
|
+
remediation_hint="Save context first or provide a valid context file path.",
|
|
654
|
+
)
|
|
655
|
+
return build_dashboard_state(session_id, db_path=db_path, api_origin=api_origin)
|
|
656
|
+
|
|
657
|
+
try:
|
|
658
|
+
snapshot_payload = json.loads(source_path.read_text(encoding="utf-8"))
|
|
659
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
660
|
+
message = f"Context load failed: {exc}."
|
|
661
|
+
merge_metadata(
|
|
662
|
+
session_id,
|
|
663
|
+
db_path=db_path,
|
|
664
|
+
context_load_status=message,
|
|
665
|
+
context_file_input=str(source_path),
|
|
666
|
+
latest_error_text=message,
|
|
667
|
+
remediation_hint="Ensure the context file is valid JSON saved by this dashboard.",
|
|
668
|
+
)
|
|
669
|
+
return build_dashboard_state(session_id, db_path=db_path, api_origin=api_origin)
|
|
670
|
+
|
|
671
|
+
loaded_state = (
|
|
672
|
+
snapshot_payload.get("state") if isinstance(snapshot_payload, dict) else {}
|
|
673
|
+
)
|
|
674
|
+
if not isinstance(loaded_state, dict):
|
|
675
|
+
loaded_state = {}
|
|
676
|
+
|
|
677
|
+
session_row = ensure_dashboard_session(session_id, db_path=db_path)
|
|
678
|
+
metadata = parse_json_blob(session_row.get("metadata_json"))
|
|
679
|
+
_RESTORABLE_KEYS = [
|
|
680
|
+
"workflow_mode",
|
|
681
|
+
"assumptions_text",
|
|
682
|
+
"active_model_path",
|
|
683
|
+
"active_model_status",
|
|
684
|
+
"feature_target_text",
|
|
685
|
+
"feature_target_status",
|
|
686
|
+
"normalized_brief",
|
|
687
|
+
"user_clarification_answer",
|
|
688
|
+
"model_provider",
|
|
689
|
+
"model_profile",
|
|
690
|
+
"model_name",
|
|
691
|
+
"local_endpoint",
|
|
692
|
+
"rag_source_path",
|
|
693
|
+
"rag_namespace",
|
|
694
|
+
"notes_text",
|
|
695
|
+
"docs_query",
|
|
696
|
+
"docs_context_text",
|
|
697
|
+
]
|
|
698
|
+
for key in _RESTORABLE_KEYS:
|
|
699
|
+
if key in loaded_state:
|
|
700
|
+
metadata[key] = loaded_state.get(key)
|
|
701
|
+
|
|
702
|
+
upsert_design_session(
|
|
703
|
+
session_id=session_id,
|
|
704
|
+
user_goal=sanitize_ui_text(
|
|
705
|
+
loaded_state.get("user_goal"),
|
|
706
|
+
session_row.get("user_goal") or DEFAULT_USER_GOAL,
|
|
707
|
+
),
|
|
708
|
+
source_mode=session_row.get("source_mode") or DEFAULT_SOURCE_MODE,
|
|
709
|
+
accepted_family=(
|
|
710
|
+
sanitize_ui_text(loaded_state.get("accepted_family"), "")
|
|
711
|
+
or session_row.get("accepted_family")
|
|
712
|
+
),
|
|
713
|
+
status=session_row.get("status") or "active",
|
|
714
|
+
current_checkpoint_index=session_row.get("current_checkpoint_index") or 0,
|
|
715
|
+
metadata_json=json.dumps(metadata, ensure_ascii=True),
|
|
716
|
+
db_path=db_path,
|
|
717
|
+
)
|
|
718
|
+
|
|
719
|
+
message = f"Context loaded from {source_path}."
|
|
720
|
+
persist_ui_action(
|
|
721
|
+
session_id,
|
|
722
|
+
tool_name="ui.context.load",
|
|
723
|
+
db_path=db_path,
|
|
724
|
+
metadata_updates={
|
|
725
|
+
"context_load_status": message,
|
|
726
|
+
"context_name_input": safe_context_name(source_path.stem, session_id),
|
|
727
|
+
"context_file_input": str(source_path),
|
|
728
|
+
"last_context_file": str(source_path),
|
|
729
|
+
"latest_message": message,
|
|
730
|
+
"latest_error_text": "",
|
|
731
|
+
"remediation_hint": "",
|
|
732
|
+
},
|
|
733
|
+
input_payload={"context_file": str(source_path)},
|
|
734
|
+
)
|
|
735
|
+
return build_dashboard_state(session_id, db_path=db_path, api_origin=api_origin)
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
# ---------------------------------------------------------------------------
|
|
739
|
+
# Readiness check
|
|
740
|
+
# ---------------------------------------------------------------------------
|
|
741
|
+
|
|
742
|
+
|
|
743
|
+
def _compute_readiness(
|
|
744
|
+
metadata: dict[str, Any],
|
|
745
|
+
*,
|
|
746
|
+
db_ready: bool,
|
|
747
|
+
) -> dict[str, Any]:
|
|
748
|
+
"""Compute the four readiness signals used by the status badge row.
|
|
749
|
+
|
|
750
|
+
Args:
|
|
751
|
+
metadata: Session metadata dict.
|
|
752
|
+
db_ready: Whether the session database row was found.
|
|
753
|
+
|
|
754
|
+
Returns:
|
|
755
|
+
Dict with ``readiness_*`` keys consumed by ``DashboardUIState``.
|
|
756
|
+
"""
|
|
757
|
+
import os # local import
|
|
758
|
+
|
|
759
|
+
provider = sanitize_ui_text(metadata.get("model_provider"), "").lower()
|
|
760
|
+
model_name = normalize_model_name_for_provider(
|
|
761
|
+
metadata.get("model_name"),
|
|
762
|
+
provider=provider or None,
|
|
763
|
+
profile=sanitize_ui_text(metadata.get("model_profile"), "balanced"),
|
|
764
|
+
)
|
|
765
|
+
local_endpoint = sanitize_ui_text(
|
|
766
|
+
metadata.get("local_endpoint"),
|
|
767
|
+
os.getenv("SOLIDWORKS_UI_LOCAL_ENDPOINT", "http://127.0.0.1:11434/v1"),
|
|
768
|
+
)
|
|
769
|
+
|
|
770
|
+
provider_configured = provider_has_credentials(model_name, local_endpoint)
|
|
771
|
+
|
|
772
|
+
try:
|
|
773
|
+
config = load_config()
|
|
774
|
+
adapter_mode = str(getattr(config.adapter_type, "value", config.adapter_type))
|
|
775
|
+
except Exception:
|
|
776
|
+
adapter_mode = "unknown"
|
|
777
|
+
|
|
778
|
+
preview_dir = ensure_preview_dir()
|
|
779
|
+
import os as _os
|
|
780
|
+
|
|
781
|
+
preview_ready = preview_dir.exists() and _os.access(preview_dir, _os.W_OK)
|
|
782
|
+
|
|
783
|
+
checks = {
|
|
784
|
+
"provider": provider_configured,
|
|
785
|
+
"adapter": adapter_mode != "unknown",
|
|
786
|
+
"preview": preview_ready,
|
|
787
|
+
"db": db_ready,
|
|
788
|
+
}
|
|
789
|
+
ready_count = sum(1 for v in checks.values() if v)
|
|
790
|
+
summary = (
|
|
791
|
+
f"Readiness {ready_count}/4 | provider={provider_configured} | "
|
|
792
|
+
f"adapter={adapter_mode} | preview={preview_ready} | db={db_ready}"
|
|
793
|
+
)
|
|
794
|
+
|
|
795
|
+
return {
|
|
796
|
+
"readiness_provider_configured": provider_configured,
|
|
797
|
+
"readiness_adapter_mode": adapter_mode,
|
|
798
|
+
"readiness_preview_ready": preview_ready,
|
|
799
|
+
"readiness_db_ready": db_ready,
|
|
800
|
+
"readiness_summary": summary,
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
|
|
804
|
+
# ---------------------------------------------------------------------------
|
|
805
|
+
# State assembly
|
|
806
|
+
# ---------------------------------------------------------------------------
|
|
807
|
+
|
|
808
|
+
# Feature-tree metadata entry names / types to strip from the UI table.
|
|
809
|
+
_META_NAMES = frozenset(
|
|
810
|
+
{
|
|
811
|
+
"sensors",
|
|
812
|
+
"annotations",
|
|
813
|
+
"history",
|
|
814
|
+
"design binder",
|
|
815
|
+
"solid bodies",
|
|
816
|
+
"surface bodies",
|
|
817
|
+
"lights, cameras and scene",
|
|
818
|
+
"equations",
|
|
819
|
+
"favorites",
|
|
820
|
+
"selection sets",
|
|
821
|
+
"3d views",
|
|
822
|
+
}
|
|
823
|
+
)
|
|
824
|
+
_META_TYPES = frozenset(
|
|
825
|
+
{
|
|
826
|
+
"sensorfolder",
|
|
827
|
+
"annotationfolder",
|
|
828
|
+
"historyfolder",
|
|
829
|
+
"designbinder",
|
|
830
|
+
"solidbodyfolder",
|
|
831
|
+
"surfacebodyfolder",
|
|
832
|
+
"lightsfolder",
|
|
833
|
+
"mategroup",
|
|
834
|
+
}
|
|
835
|
+
)
|
|
836
|
+
|
|
837
|
+
|
|
838
|
+
def _planned_tools(planned: dict[str, Any]) -> list[str]:
|
|
839
|
+
"""Extract the tool-name list from a checkpoint planned-action dict.
|
|
840
|
+
|
|
841
|
+
Args:
|
|
842
|
+
planned: Parsed planned-action JSON dict.
|
|
843
|
+
|
|
844
|
+
Returns:
|
|
845
|
+
List of tool name strings.
|
|
846
|
+
"""
|
|
847
|
+
tools = planned.get("tools", [])
|
|
848
|
+
return [str(tool) for tool in tools] if isinstance(tools, list) else []
|
|
849
|
+
|
|
850
|
+
|
|
851
|
+
def _build_checkpoint_rows(
|
|
852
|
+
session_id: str,
|
|
853
|
+
*,
|
|
854
|
+
db_path: Path | None,
|
|
855
|
+
is_new_design_clean: bool,
|
|
856
|
+
) -> list[dict[str, Any]]:
|
|
857
|
+
"""Assemble the checkpoint list for the dashboard state payload.
|
|
858
|
+
|
|
859
|
+
Args:
|
|
860
|
+
session_id: Dashboard session identifier.
|
|
861
|
+
db_path: Optional SQLite override.
|
|
862
|
+
is_new_design_clean: Whether this is a fresh new-design session.
|
|
863
|
+
|
|
864
|
+
Returns:
|
|
865
|
+
List of :class:`DashboardCheckpoint` dicts.
|
|
866
|
+
"""
|
|
867
|
+
checkpoints: list[dict[str, Any]] = []
|
|
868
|
+
for row in list_plan_checkpoints(session_id, db_path=db_path):
|
|
869
|
+
planned = parse_json_blob(row["planned_action_json"])
|
|
870
|
+
result_payload = parse_json_blob(row.get("result_json"))
|
|
871
|
+
if result_payload.get("status") == "error":
|
|
872
|
+
status = "failed"
|
|
873
|
+
elif row["executed"]:
|
|
874
|
+
status = "executed"
|
|
875
|
+
elif row["approved_by_user"]:
|
|
876
|
+
status = "approved"
|
|
877
|
+
else:
|
|
878
|
+
status = "queued"
|
|
879
|
+
|
|
880
|
+
if is_new_design_clean and not row["executed"]:
|
|
881
|
+
status = "queued"
|
|
882
|
+
|
|
883
|
+
mocked_tools = result_payload.get("mocked_tools", [])
|
|
884
|
+
tools_text = ", ".join(_planned_tools(planned))
|
|
885
|
+
if mocked_tools:
|
|
886
|
+
tools_text = f"{tools_text} [MOCKED: {', '.join(mocked_tools)}]"
|
|
887
|
+
|
|
888
|
+
checkpoints.append(
|
|
889
|
+
DashboardCheckpoint(
|
|
890
|
+
step=str(row["checkpoint_index"]),
|
|
891
|
+
goal=planned.get("goal") or row["title"],
|
|
892
|
+
tools=tools_text,
|
|
893
|
+
status=status,
|
|
894
|
+
).model_dump()
|
|
895
|
+
)
|
|
896
|
+
return checkpoints
|
|
897
|
+
|
|
898
|
+
|
|
899
|
+
def _build_evidence_rows(
|
|
900
|
+
session_id: str,
|
|
901
|
+
*,
|
|
902
|
+
db_path: Path | None,
|
|
903
|
+
active_model_path: str,
|
|
904
|
+
is_new_design_clean: bool,
|
|
905
|
+
) -> list[dict[str, Any]]:
|
|
906
|
+
"""Assemble the filtered evidence-link rows for the dashboard state payload.
|
|
907
|
+
|
|
908
|
+
Args:
|
|
909
|
+
session_id: Dashboard session identifier.
|
|
910
|
+
db_path: Optional SQLite override.
|
|
911
|
+
active_model_path: Currently attached model path.
|
|
912
|
+
is_new_design_clean: Whether this is a fresh new-design session.
|
|
913
|
+
|
|
914
|
+
Returns:
|
|
915
|
+
List of :class:`DashboardEvidenceRow` dicts.
|
|
916
|
+
"""
|
|
917
|
+
if is_new_design_clean:
|
|
918
|
+
return []
|
|
919
|
+
|
|
920
|
+
all_evidence = list_evidence_links(session_id, db_path=db_path)
|
|
921
|
+
model_scoped_sources = {"active_model", "feature_target"}
|
|
922
|
+
|
|
923
|
+
filtered_evidence: list[dict[str, Any]] = []
|
|
924
|
+
for evidence in all_evidence:
|
|
925
|
+
source_type = str(evidence.get("source_type") or "")
|
|
926
|
+
source_id = str(evidence.get("source_id") or "")
|
|
927
|
+
if (
|
|
928
|
+
source_type in model_scoped_sources
|
|
929
|
+
and active_model_path
|
|
930
|
+
and source_id
|
|
931
|
+
and source_id != active_model_path
|
|
932
|
+
):
|
|
933
|
+
continue
|
|
934
|
+
filtered_evidence.append(evidence)
|
|
935
|
+
|
|
936
|
+
# Collapse feature-target rows to show only the latest so the table reflects
|
|
937
|
+
# the current target rather than stale historical misses.
|
|
938
|
+
latest_feature_target: dict[str, Any] | None = None
|
|
939
|
+
compact_evidence: list[dict[str, Any]] = []
|
|
940
|
+
for evidence in filtered_evidence:
|
|
941
|
+
if str(evidence.get("source_type") or "") == "feature_target":
|
|
942
|
+
latest_feature_target = evidence
|
|
943
|
+
continue
|
|
944
|
+
compact_evidence.append(evidence)
|
|
945
|
+
if latest_feature_target is not None:
|
|
946
|
+
compact_evidence.append(latest_feature_target)
|
|
947
|
+
|
|
948
|
+
rows: list[dict[str, Any]] = []
|
|
949
|
+
for evidence in compact_evidence[-6:]:
|
|
950
|
+
rows.append(
|
|
951
|
+
DashboardEvidenceRow(
|
|
952
|
+
source=evidence["source_type"],
|
|
953
|
+
detail=evidence["rationale"] or evidence["source_id"],
|
|
954
|
+
score=(
|
|
955
|
+
f"{evidence['relevance_score']:.2f}"
|
|
956
|
+
if evidence["relevance_score"] is not None
|
|
957
|
+
else "-"
|
|
958
|
+
),
|
|
959
|
+
).model_dump()
|
|
960
|
+
)
|
|
961
|
+
return rows
|
|
962
|
+
|
|
963
|
+
|
|
964
|
+
def _build_feature_tree(
|
|
965
|
+
session_id: str,
|
|
966
|
+
*,
|
|
967
|
+
db_path: Path | None,
|
|
968
|
+
is_new_design_clean: bool,
|
|
969
|
+
selected_feature_name: str,
|
|
970
|
+
) -> list[dict[str, Any]]:
|
|
971
|
+
"""Load and filter the feature tree from the most recent snapshot.
|
|
972
|
+
|
|
973
|
+
Args:
|
|
974
|
+
session_id: Dashboard session identifier.
|
|
975
|
+
db_path: Optional SQLite override.
|
|
976
|
+
is_new_design_clean: When ``True``, returns an empty list.
|
|
977
|
+
selected_feature_name: Currently selected feature (adds ``_selected`` marker).
|
|
978
|
+
|
|
979
|
+
Returns:
|
|
980
|
+
List of feature tree row dicts.
|
|
981
|
+
"""
|
|
982
|
+
if is_new_design_clean:
|
|
983
|
+
return []
|
|
984
|
+
|
|
985
|
+
feature_tree_items: list[dict[str, Any]] = []
|
|
986
|
+
for snap in list_model_state_snapshots(session_id, db_path=db_path):
|
|
987
|
+
raw_tree = snap.get("feature_tree_json")
|
|
988
|
+
if raw_tree:
|
|
989
|
+
try:
|
|
990
|
+
parsed = json.loads(raw_tree)
|
|
991
|
+
if isinstance(parsed, list):
|
|
992
|
+
feature_tree_items = [
|
|
993
|
+
f
|
|
994
|
+
for f in parsed
|
|
995
|
+
if f.get("name", "").lower() not in _META_NAMES
|
|
996
|
+
and f.get("type", "").lower() not in _META_TYPES
|
|
997
|
+
]
|
|
998
|
+
break
|
|
999
|
+
except Exception:
|
|
1000
|
+
pass
|
|
1001
|
+
|
|
1002
|
+
if selected_feature_name:
|
|
1003
|
+
feature_tree_items = [
|
|
1004
|
+
{**f, "_selected": "●" if f.get("name") == selected_feature_name else ""}
|
|
1005
|
+
for f in feature_tree_items
|
|
1006
|
+
]
|
|
1007
|
+
return feature_tree_items
|
|
1008
|
+
|
|
1009
|
+
|
|
1010
|
+
def build_dashboard_state(
|
|
1011
|
+
session_id: str = DEFAULT_SESSION_ID,
|
|
1012
|
+
*,
|
|
1013
|
+
db_path: Path | None = None,
|
|
1014
|
+
api_origin: str = DEFAULT_API_ORIGIN,
|
|
1015
|
+
) -> dict[str, Any]:
|
|
1016
|
+
"""Assemble the complete dashboard payload consumed by the Prefab UI renderer.
|
|
1017
|
+
|
|
1018
|
+
This function is the single read-path for all UI state: it reads the session
|
|
1019
|
+
database, merges every sub-component (checkpoints, evidence, snapshots, preview
|
|
1020
|
+
URLs, provider readiness), and returns the ``DashboardUIState`` model as a dict.
|
|
1021
|
+
|
|
1022
|
+
Args:
|
|
1023
|
+
session_id: Dashboard session identifier.
|
|
1024
|
+
db_path: Optional override for the SQLite database path.
|
|
1025
|
+
api_origin: Base URL used to construct preview and viewer URLs.
|
|
1026
|
+
|
|
1027
|
+
Returns:
|
|
1028
|
+
``DashboardUIState`` model dumped to a plain dict.
|
|
1029
|
+
"""
|
|
1030
|
+
import os # local import
|
|
1031
|
+
|
|
1032
|
+
session_row = ensure_dashboard_session(session_id, db_path=db_path)
|
|
1033
|
+
metadata = parse_json_blob(session_row.get("metadata_json"))
|
|
1034
|
+
db_ready = bool(session_row)
|
|
1035
|
+
workflow_mode = normalize_workflow_mode(metadata.get("workflow_mode"))
|
|
1036
|
+
active_model_path = sanitize_model_path_text(metadata.get("active_model_path"))
|
|
1037
|
+
is_new_design_clean = workflow_mode == "new_design" and not active_model_path
|
|
1038
|
+
|
|
1039
|
+
# --- Checkpoints ---
|
|
1040
|
+
checkpoints = _build_checkpoint_rows(
|
|
1041
|
+
session_id, db_path=db_path, is_new_design_clean=is_new_design_clean
|
|
1042
|
+
)
|
|
1043
|
+
structured_rendering_enabled = bool(checkpoints)
|
|
1044
|
+
checkpoints_text = (
|
|
1045
|
+
" | ".join(
|
|
1046
|
+
f"{item['step']}. {item['goal']} [{item['status']}] via {item['tools']}"
|
|
1047
|
+
for item in checkpoints
|
|
1048
|
+
)
|
|
1049
|
+
if checkpoints
|
|
1050
|
+
else "No checkpoints available yet."
|
|
1051
|
+
)
|
|
1052
|
+
|
|
1053
|
+
# --- Evidence rows ---
|
|
1054
|
+
evidence_rows = _build_evidence_rows(
|
|
1055
|
+
session_id,
|
|
1056
|
+
db_path=db_path,
|
|
1057
|
+
active_model_path=active_model_path,
|
|
1058
|
+
is_new_design_clean=is_new_design_clean,
|
|
1059
|
+
)
|
|
1060
|
+
evidence_rows_text = (
|
|
1061
|
+
" | ".join(
|
|
1062
|
+
f"{item['source']}: {item['detail']} (score {item['score']})"
|
|
1063
|
+
for item in evidence_rows
|
|
1064
|
+
)
|
|
1065
|
+
if evidence_rows
|
|
1066
|
+
else "No evidence links captured yet."
|
|
1067
|
+
)
|
|
1068
|
+
|
|
1069
|
+
# --- Tool history ---
|
|
1070
|
+
tool_history = list_tool_call_records(session_id, db_path=db_path)
|
|
1071
|
+
latest_tool = tool_history[-1]["tool_name"] if tool_history else "waiting"
|
|
1072
|
+
tool_history_text = trace_json(trace_tool_records(tool_history[-20:]))
|
|
1073
|
+
|
|
1074
|
+
# --- Preview URL ---
|
|
1075
|
+
|
|
1076
|
+
preview_url = ""
|
|
1077
|
+
preview_status = "No preview captured yet."
|
|
1078
|
+
snapshots = list_model_state_snapshots(session_id, db_path=db_path)
|
|
1079
|
+
latest_snapshot_path = snapshots[0].get("screenshot_path") if snapshots else None
|
|
1080
|
+
if latest_snapshot_path:
|
|
1081
|
+
preview_path = Path(latest_snapshot_path)
|
|
1082
|
+
if preview_path.exists():
|
|
1083
|
+
ts = int(preview_path.stat().st_mtime)
|
|
1084
|
+
preview_url = f"{api_origin}/previews/{preview_path.name}?ts={ts}"
|
|
1085
|
+
preview_status = (
|
|
1086
|
+
f"Synced from SolidWorks current view. Last file: {preview_path.name}"
|
|
1087
|
+
)
|
|
1088
|
+
|
|
1089
|
+
# --- Feature tree ---
|
|
1090
|
+
selected_feature_name = str(metadata.get("selected_feature_name") or "")
|
|
1091
|
+
feature_tree_items = _build_feature_tree(
|
|
1092
|
+
session_id,
|
|
1093
|
+
db_path=db_path,
|
|
1094
|
+
is_new_design_clean=is_new_design_clean,
|
|
1095
|
+
selected_feature_name=selected_feature_name,
|
|
1096
|
+
)
|
|
1097
|
+
|
|
1098
|
+
# --- 3D viewer URL ---
|
|
1099
|
+
preview_viewer_url = sanitize_preview_viewer_url(
|
|
1100
|
+
metadata.get("preview_viewer_url"),
|
|
1101
|
+
session_id=session_id,
|
|
1102
|
+
api_origin=api_origin,
|
|
1103
|
+
)
|
|
1104
|
+
if (
|
|
1105
|
+
not preview_viewer_url
|
|
1106
|
+
and bool(metadata.get("preview_stl_ready"))
|
|
1107
|
+
and metadata.get("active_model_path")
|
|
1108
|
+
):
|
|
1109
|
+
preview_viewer_url = (
|
|
1110
|
+
f"{api_origin}/api/ui/viewer/{session_id}?session_id={session_id}&t=0"
|
|
1111
|
+
)
|
|
1112
|
+
|
|
1113
|
+
preview_status = sanitize_ui_text(metadata.get("preview_status"), preview_status)
|
|
1114
|
+
|
|
1115
|
+
# --- Family / clarification ---
|
|
1116
|
+
family = (
|
|
1117
|
+
session_row.get("accepted_family")
|
|
1118
|
+
or metadata.get("proposed_family")
|
|
1119
|
+
or "unclassified"
|
|
1120
|
+
)
|
|
1121
|
+
confidence = metadata.get("family_confidence", "pending")
|
|
1122
|
+
evidence_text = (
|
|
1123
|
+
" | ".join(metadata.get("family_evidence", [])) or "No family evidence yet."
|
|
1124
|
+
)
|
|
1125
|
+
warning_text = (
|
|
1126
|
+
" | ".join(metadata.get("family_warnings", [])) or "No blocking warnings."
|
|
1127
|
+
)
|
|
1128
|
+
questions = metadata.get("clarifying_questions", [])
|
|
1129
|
+
question_text = (
|
|
1130
|
+
"\n".join(f"- {item}" for item in questions)
|
|
1131
|
+
if questions
|
|
1132
|
+
else "No outstanding clarification questions."
|
|
1133
|
+
)
|
|
1134
|
+
|
|
1135
|
+
# --- Model / provider ---
|
|
1136
|
+
model_name = sanitize_ui_text(
|
|
1137
|
+
metadata.get("model_name"),
|
|
1138
|
+
os.getenv("SOLIDWORKS_UI_MODEL", "github:openai/gpt-4.1"),
|
|
1139
|
+
)
|
|
1140
|
+
model_provider = str(
|
|
1141
|
+
metadata.get("model_provider") or provider_from_model_name(model_name)
|
|
1142
|
+
)
|
|
1143
|
+
model_profile = str(metadata.get("model_profile") or "balanced")
|
|
1144
|
+
|
|
1145
|
+
# --- Active model status ---
|
|
1146
|
+
active_model_status = sanitize_ui_text(metadata.get("active_model_status"), "")
|
|
1147
|
+
if active_model_path and not active_model_status:
|
|
1148
|
+
active_model_status = (
|
|
1149
|
+
f"Model path set: {Path(active_model_path).name} (connect pending)."
|
|
1150
|
+
)
|
|
1151
|
+
if not active_model_path and not active_model_status:
|
|
1152
|
+
active_model_status = "No active model connected yet."
|
|
1153
|
+
|
|
1154
|
+
# --- Workflow copy ---
|
|
1155
|
+
workflow_label, workflow_guidance_text, flow_header_text = workflow_copy(
|
|
1156
|
+
workflow_mode, active_model_path
|
|
1157
|
+
)
|
|
1158
|
+
|
|
1159
|
+
# --- Local model ---
|
|
1160
|
+
local_endpoint = sanitize_ui_text(
|
|
1161
|
+
metadata.get("local_endpoint"),
|
|
1162
|
+
os.getenv("SOLIDWORKS_UI_LOCAL_ENDPOINT", "http://127.0.0.1:11434/v1"),
|
|
1163
|
+
)
|
|
1164
|
+
|
|
1165
|
+
# --- Readiness ---
|
|
1166
|
+
readiness = _compute_readiness(metadata, db_ready=db_ready)
|
|
1167
|
+
|
|
1168
|
+
# --- Context text ---
|
|
1169
|
+
active_model_name = Path(active_model_path).name if active_model_path else "<none>"
|
|
1170
|
+
preview_views = metadata.get("preview_view_urls") or {}
|
|
1171
|
+
model_context_lines = [
|
|
1172
|
+
f"Model file: {active_model_name}",
|
|
1173
|
+
f"Absolute path: {active_model_path or '<none>'}",
|
|
1174
|
+
f"Model type: {str(metadata.get('active_model_type') or '<unknown>')}",
|
|
1175
|
+
f"Configuration: {str(metadata.get('active_model_configuration') or '<unknown>')}",
|
|
1176
|
+
f"Feature tree rows: {len(feature_tree_items)}",
|
|
1177
|
+
f"Selected feature: {selected_feature_name or '<none>'}",
|
|
1178
|
+
f"Feature targets: {str(metadata.get('feature_target_text') or '<none>')}",
|
|
1179
|
+
f"Preview views captured: {', '.join(sorted(preview_views.keys())) or '<none>'}",
|
|
1180
|
+
f"Latest preview status: {preview_status}",
|
|
1181
|
+
]
|
|
1182
|
+
model_context_text = "\n".join(model_context_lines)
|
|
1183
|
+
context_summary = (
|
|
1184
|
+
f"{active_model_name} | {str(metadata.get('active_model_type') or 'unknown')}"
|
|
1185
|
+
f" | config {str(metadata.get('active_model_configuration') or '<unknown>')}"
|
|
1186
|
+
f" | features {len(feature_tree_items)}"
|
|
1187
|
+
)
|
|
1188
|
+
|
|
1189
|
+
fg_warning = feature_grounding_warning_text(
|
|
1190
|
+
active_model_path=active_model_path,
|
|
1191
|
+
feature_target_text=str(metadata.get("feature_target_text") or ""),
|
|
1192
|
+
feature_tree_count=len(feature_tree_items),
|
|
1193
|
+
)
|
|
1194
|
+
|
|
1195
|
+
canonical_prompt_text = "\n".join(
|
|
1196
|
+
[
|
|
1197
|
+
f"Goal: {session_row.get('user_goal') or DEFAULT_USER_GOAL}",
|
|
1198
|
+
f"Assumptions: {sanitize_ui_text(metadata.get('assumptions_text'), '') or '<none>'}",
|
|
1199
|
+
f"Active model path: {active_model_path or '<none>'}",
|
|
1200
|
+
f"Active model status: {active_model_status}",
|
|
1201
|
+
f"Feature targets: {str(metadata.get('feature_target_text') or '<none>')}",
|
|
1202
|
+
f"Feature target status: {str(metadata.get('feature_target_status') or '<none>')}",
|
|
1203
|
+
f"Accepted/proposed family: {session_row.get('accepted_family') or metadata.get('proposed_family') or '<none>'}",
|
|
1204
|
+
f"RAG provenance: {str(metadata.get('rag_provenance_text') or '<none>')}",
|
|
1205
|
+
f"Docs context: {str(metadata.get('docs_context_text') or '<none>')}",
|
|
1206
|
+
f"Engineering notes: {str(metadata.get('notes_text') or '<none>')}",
|
|
1207
|
+
]
|
|
1208
|
+
)
|
|
1209
|
+
|
|
1210
|
+
state = DashboardUIState(
|
|
1211
|
+
session_id=session_id,
|
|
1212
|
+
workflow_mode=workflow_mode,
|
|
1213
|
+
workflow_label=workflow_label,
|
|
1214
|
+
workflow_guidance_text=workflow_guidance_text,
|
|
1215
|
+
user_goal=session_row.get("user_goal") or DEFAULT_USER_GOAL,
|
|
1216
|
+
flow_header_text=flow_header_text,
|
|
1217
|
+
assumptions_text=sanitize_ui_text(
|
|
1218
|
+
metadata.get("assumptions_text"),
|
|
1219
|
+
"Assume PETG, 0.4mm nozzle, 0.2mm layers, and 0.30mm mating clearance unless overridden.",
|
|
1220
|
+
),
|
|
1221
|
+
active_model_path=active_model_path,
|
|
1222
|
+
active_model_status=active_model_status,
|
|
1223
|
+
active_model_type=str(metadata.get("active_model_type") or ""),
|
|
1224
|
+
active_model_configuration=str(
|
|
1225
|
+
metadata.get("active_model_configuration") or ""
|
|
1226
|
+
),
|
|
1227
|
+
feature_target_text=str(metadata.get("feature_target_text") or ""),
|
|
1228
|
+
feature_target_status=str(
|
|
1229
|
+
metadata.get("feature_target_status")
|
|
1230
|
+
or "No grounded feature target selected."
|
|
1231
|
+
),
|
|
1232
|
+
feature_grounding_warning_text=fg_warning,
|
|
1233
|
+
normalized_brief=(
|
|
1234
|
+
metadata.get("normalized_brief")
|
|
1235
|
+
or session_row.get("user_goal")
|
|
1236
|
+
or DEFAULT_USER_GOAL
|
|
1237
|
+
),
|
|
1238
|
+
clarifying_questions_text=question_text,
|
|
1239
|
+
proposed_family=family,
|
|
1240
|
+
family_confidence=confidence,
|
|
1241
|
+
family_evidence_text=evidence_text,
|
|
1242
|
+
family_warning_text=warning_text,
|
|
1243
|
+
accepted_family=session_row.get("accepted_family") or "",
|
|
1244
|
+
checkpoints=checkpoints, # type: ignore[arg-type]
|
|
1245
|
+
checkpoints_text=checkpoints_text,
|
|
1246
|
+
evidence_rows=evidence_rows, # type: ignore[arg-type]
|
|
1247
|
+
evidence_rows_text=evidence_rows_text,
|
|
1248
|
+
structured_rendering_enabled=structured_rendering_enabled,
|
|
1249
|
+
manual_sync_ready=False,
|
|
1250
|
+
preview_url=preview_url,
|
|
1251
|
+
preview_status=preview_status,
|
|
1252
|
+
preview_orientation=metadata.get(
|
|
1253
|
+
"preview_orientation", DEFAULT_PREVIEW_ORIENTATION
|
|
1254
|
+
),
|
|
1255
|
+
latest_message=metadata.get("latest_message", "Ready."),
|
|
1256
|
+
latest_tool=latest_tool,
|
|
1257
|
+
latest_error_text=str(metadata.get("latest_error_text") or ""),
|
|
1258
|
+
remediation_hint=str(metadata.get("remediation_hint") or ""),
|
|
1259
|
+
model_provider=model_provider,
|
|
1260
|
+
model_name=model_name,
|
|
1261
|
+
model_profile=model_profile,
|
|
1262
|
+
local_endpoint=local_endpoint,
|
|
1263
|
+
local_model_status_text=str(
|
|
1264
|
+
metadata.get("local_model_status_text") or "Local model controls idle."
|
|
1265
|
+
),
|
|
1266
|
+
local_model_busy=bool(metadata.get("local_model_busy") or False),
|
|
1267
|
+
local_model_available=bool(metadata.get("local_model_available") or False),
|
|
1268
|
+
local_model_recommended_tier=str(
|
|
1269
|
+
metadata.get("local_model_recommended_tier") or ""
|
|
1270
|
+
),
|
|
1271
|
+
local_model_recommended_ollama_model=str(
|
|
1272
|
+
metadata.get("local_model_recommended_ollama_model") or ""
|
|
1273
|
+
),
|
|
1274
|
+
local_model_pull_command=str(metadata.get("local_model_pull_command") or ""),
|
|
1275
|
+
local_model_label=str(metadata.get("local_model_label") or ""),
|
|
1276
|
+
rag_source_path=str(metadata.get("rag_source_path") or ""),
|
|
1277
|
+
rag_namespace=str(metadata.get("rag_namespace") or "engineering-reference"),
|
|
1278
|
+
rag_status=str(
|
|
1279
|
+
metadata.get("rag_status") or "No retrieval source ingested yet."
|
|
1280
|
+
),
|
|
1281
|
+
rag_index_path=str(metadata.get("rag_index_path") or ""),
|
|
1282
|
+
rag_chunk_count=int(metadata.get("rag_chunk_count") or 0),
|
|
1283
|
+
rag_provenance_text=str(
|
|
1284
|
+
metadata.get("rag_provenance_text")
|
|
1285
|
+
or "No retrieval provenance available yet."
|
|
1286
|
+
),
|
|
1287
|
+
docs_query=str(metadata.get("docs_query") or "SolidWorks MCP endpoints"),
|
|
1288
|
+
docs_context_text=str(
|
|
1289
|
+
metadata.get("docs_context_text") or "No docs context loaded yet."
|
|
1290
|
+
),
|
|
1291
|
+
notes_text=str(metadata.get("notes_text") or ""),
|
|
1292
|
+
orchestration_status=str(metadata.get("orchestration_status") or "Ready."),
|
|
1293
|
+
context_save_status=str(metadata.get("context_save_status") or ""),
|
|
1294
|
+
context_load_status=str(metadata.get("context_load_status") or ""),
|
|
1295
|
+
context_name_input=str(metadata.get("context_name_input") or session_id),
|
|
1296
|
+
context_file_input=str(metadata.get("last_context_file") or ""),
|
|
1297
|
+
readiness_provider_configured=readiness["readiness_provider_configured"],
|
|
1298
|
+
readiness_adapter_mode=readiness["readiness_adapter_mode"],
|
|
1299
|
+
readiness_preview_ready=readiness["readiness_preview_ready"],
|
|
1300
|
+
readiness_db_ready=readiness["readiness_db_ready"],
|
|
1301
|
+
readiness_summary=readiness["readiness_summary"],
|
|
1302
|
+
context_used_pct=38,
|
|
1303
|
+
context_text=context_summary,
|
|
1304
|
+
model_context_text=model_context_text,
|
|
1305
|
+
canonical_prompt_text=canonical_prompt_text,
|
|
1306
|
+
tool_history_text=tool_history_text,
|
|
1307
|
+
api_origin=api_origin,
|
|
1308
|
+
preview_viewer_url=preview_viewer_url,
|
|
1309
|
+
preview_view_urls=metadata.get("preview_view_urls") or {},
|
|
1310
|
+
user_clarification_answer=str(metadata.get("user_clarification_answer") or ""),
|
|
1311
|
+
mocked_tools_text=(
|
|
1312
|
+
"MOCKED tools: " + ", ".join(metadata.get("mocked_tools", []))
|
|
1313
|
+
if metadata.get("mocked_tools")
|
|
1314
|
+
else ""
|
|
1315
|
+
),
|
|
1316
|
+
feature_tree_items=feature_tree_items,
|
|
1317
|
+
selected_feature_name=str(metadata.get("selected_feature_name") or ""),
|
|
1318
|
+
).model_dump()
|
|
1319
|
+
|
|
1320
|
+
logger.debug(
|
|
1321
|
+
"[ui.trace.state] session_id={} model_path={} selected={} feature_rows={} preview_views={} latest_tool={}",
|
|
1322
|
+
session_id,
|
|
1323
|
+
state.get("active_model_path") or "<none>",
|
|
1324
|
+
state.get("selected_feature_name") or "<none>",
|
|
1325
|
+
len(state.get("feature_tree_items") or []),
|
|
1326
|
+
list((state.get("preview_view_urls") or {}).keys()),
|
|
1327
|
+
state.get("latest_tool") or "waiting",
|
|
1328
|
+
)
|
|
1329
|
+
return state
|
|
1330
|
+
|
|
1331
|
+
|
|
1332
|
+
def build_dashboard_trace_payload(
|
|
1333
|
+
session_id: str = DEFAULT_SESSION_ID,
|
|
1334
|
+
*,
|
|
1335
|
+
db_path: Path | None = None,
|
|
1336
|
+
api_origin: str = DEFAULT_API_ORIGIN,
|
|
1337
|
+
) -> dict[str, Any]:
|
|
1338
|
+
"""Assemble the verbose debug/trace payload for the operator trace panel.
|
|
1339
|
+
|
|
1340
|
+
Includes the raw session row, full metadata, complete tool-call history, and the
|
|
1341
|
+
assembled ``DashboardUIState`` — all serialised to both Python dicts and
|
|
1342
|
+
pretty-printed JSON strings for easy inspection in the UI.
|
|
1343
|
+
|
|
1344
|
+
Args:
|
|
1345
|
+
session_id: Dashboard session identifier.
|
|
1346
|
+
db_path: Optional override for the SQLite database path.
|
|
1347
|
+
api_origin: API origin used for URL generation.
|
|
1348
|
+
|
|
1349
|
+
Returns:
|
|
1350
|
+
Dict with ``session_row``, ``metadata``, ``state``, and ``tool_records`` sections.
|
|
1351
|
+
"""
|
|
1352
|
+
ensure_dashboard_session(session_id, db_path=db_path)
|
|
1353
|
+
session_row = get_design_session(session_id, db_path=db_path) or {}
|
|
1354
|
+
metadata = parse_json_blob(session_row.get("metadata_json"))
|
|
1355
|
+
state = build_dashboard_state(session_id, db_path=db_path, api_origin=api_origin)
|
|
1356
|
+
tool_records = trace_tool_records(
|
|
1357
|
+
list_tool_call_records(session_id, db_path=db_path)
|
|
1358
|
+
)
|
|
1359
|
+
session_row_payload = trace_session_row(session_row)
|
|
1360
|
+
|
|
1361
|
+
payload = {
|
|
1362
|
+
"session_id": session_id,
|
|
1363
|
+
"workflow_mode": state.get("workflow_mode", DEFAULT_WORKFLOW_MODE),
|
|
1364
|
+
"latest_message": state.get("latest_message", "Ready."),
|
|
1365
|
+
"latest_error_text": state.get("latest_error_text", ""),
|
|
1366
|
+
"debug_summary": (
|
|
1367
|
+
f"workflow={state.get('workflow_mode', DEFAULT_WORKFLOW_MODE)}"
|
|
1368
|
+
f" | model_path={state.get('active_model_path', '') or '<none>'}"
|
|
1369
|
+
f" | latest_tool={state.get('latest_tool', 'waiting')}"
|
|
1370
|
+
f" | tool_records={len(tool_records)}"
|
|
1371
|
+
),
|
|
1372
|
+
"session_row": session_row_payload,
|
|
1373
|
+
"session_row_text": trace_json(session_row_payload),
|
|
1374
|
+
"metadata": metadata,
|
|
1375
|
+
"metadata_text": trace_json(metadata),
|
|
1376
|
+
"state": state,
|
|
1377
|
+
"state_text": trace_json(state),
|
|
1378
|
+
"tool_records": tool_records,
|
|
1379
|
+
"tool_records_text": trace_json(tool_records),
|
|
1380
|
+
}
|
|
1381
|
+
logger.debug(
|
|
1382
|
+
"[ui.trace.snapshot] session_id={} model_path={} selected={} tool_records={} preview_views={}",
|
|
1383
|
+
session_id,
|
|
1384
|
+
state.get("active_model_path") or "<none>",
|
|
1385
|
+
state.get("selected_feature_name") or "<none>",
|
|
1386
|
+
len(tool_records),
|
|
1387
|
+
list((state.get("preview_view_urls") or {}).keys()),
|
|
1388
|
+
)
|
|
1389
|
+
return payload
|