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.
Files changed (115) hide show
  1. solidworks_mcp/__init__.py +57 -0
  2. solidworks_mcp/adapters/__init__.py +40 -0
  3. solidworks_mcp/adapters/base.py +1882 -0
  4. solidworks_mcp/adapters/circuit_breaker.py +2052 -0
  5. solidworks_mcp/adapters/com_executor.py +252 -0
  6. solidworks_mcp/adapters/complexity_analyzer.py +272 -0
  7. solidworks_mcp/adapters/connection_pool.py +1884 -0
  8. solidworks_mcp/adapters/factory.py +366 -0
  9. solidworks_mcp/adapters/intelligent_router.py +218 -0
  10. solidworks_mcp/adapters/mock_adapter.py +4016 -0
  11. solidworks_mcp/adapters/pywin32_adapter.py +2842 -0
  12. solidworks_mcp/adapters/solidworks/__init__.py +17 -0
  13. solidworks_mcp/adapters/solidworks/bitmap.py +180 -0
  14. solidworks_mcp/adapters/solidworks/com_utils.py +56 -0
  15. solidworks_mcp/adapters/solidworks/dimension_layout.py +753 -0
  16. solidworks_mcp/adapters/solidworks/documentation.py +1476 -0
  17. solidworks_mcp/adapters/solidworks/drawing_standard.py +1993 -0
  18. solidworks_mcp/adapters/solidworks/face_selection.py +392 -0
  19. solidworks_mcp/adapters/solidworks/features.py +3362 -0
  20. solidworks_mcp/adapters/solidworks/io.py +3649 -0
  21. solidworks_mcp/adapters/solidworks/rebuild_diagnostics.py +269 -0
  22. solidworks_mcp/adapters/solidworks/rebuild_inspector.py +146 -0
  23. solidworks_mcp/adapters/solidworks/selection.py +308 -0
  24. solidworks_mcp/adapters/solidworks/sketch.py +2545 -0
  25. solidworks_mcp/adapters/sw_feature_errors.py +66 -0
  26. solidworks_mcp/adapters/sw_type_info.py +317 -0
  27. solidworks_mcp/adapters/vba_adapter.py +318 -0
  28. solidworks_mcp/adapters/vba_macro_executor.py +215 -0
  29. solidworks_mcp/agents/__init__.py +68 -0
  30. solidworks_mcp/agents/design_knowledge/assembly_patterns.md +127 -0
  31. solidworks_mcp/agents/design_knowledge/hinges.md +118 -0
  32. solidworks_mcp/agents/design_knowledge/snap_fits.md +93 -0
  33. solidworks_mcp/agents/design_knowledge/solidworks_basics.md +3 -0
  34. solidworks_mcp/agents/design_knowledge/tolerancing.md +128 -0
  35. solidworks_mcp/agents/harness.py +216 -0
  36. solidworks_mcp/agents/history_db.py +1508 -0
  37. solidworks_mcp/agents/retrieval_index.py +164 -0
  38. solidworks_mcp/agents/schemas.py +165 -0
  39. solidworks_mcp/agents/smoke_test.py +285 -0
  40. solidworks_mcp/agents/soc_exporter.py +650 -0
  41. solidworks_mcp/agents/soc_pickup.py +332 -0
  42. solidworks_mcp/agents/soc_rewind.py +221 -0
  43. solidworks_mcp/agents/vector_rag.py +665 -0
  44. solidworks_mcp/cache/__init__.py +5 -0
  45. solidworks_mcp/cache/response_cache.py +164 -0
  46. solidworks_mcp/config.py +729 -0
  47. solidworks_mcp/exceptions.py +37 -0
  48. solidworks_mcp/security/__init__.py +68 -0
  49. solidworks_mcp/security/auth.py +128 -0
  50. solidworks_mcp/security/cors.py +27 -0
  51. solidworks_mcp/security/rate_limiting.py +90 -0
  52. solidworks_mcp/security/runtime.py +155 -0
  53. solidworks_mcp/server.py +777 -0
  54. solidworks_mcp/server_cli_fixed.py +120 -0
  55. solidworks_mcp/setup/__init__.py +1 -0
  56. solidworks_mcp/setup/cli.py +399 -0
  57. solidworks_mcp/setup/clients.py +157 -0
  58. solidworks_mcp/setup/config_file.py +230 -0
  59. solidworks_mcp/setup/handshake.py +202 -0
  60. solidworks_mcp/setup/launcher.py +102 -0
  61. solidworks_mcp/setup/legacy.py +100 -0
  62. solidworks_mcp/setup/registration.py +193 -0
  63. solidworks_mcp/setup/solidworks.py +49 -0
  64. solidworks_mcp/tools/__init__.py +64 -0
  65. solidworks_mcp/tools/analysis.py +723 -0
  66. solidworks_mcp/tools/automation.py +578 -0
  67. solidworks_mcp/tools/docs_discovery.py +1476 -0
  68. solidworks_mcp/tools/drawing.py +1015 -0
  69. solidworks_mcp/tools/drawing_analysis.py +456 -0
  70. solidworks_mcp/tools/drawing_automation.py +627 -0
  71. solidworks_mcp/tools/engineering_data.py +466 -0
  72. solidworks_mcp/tools/error_classifier.py +232 -0
  73. solidworks_mcp/tools/export.py +833 -0
  74. solidworks_mcp/tools/file_facts.py +40 -0
  75. solidworks_mcp/tools/file_management.py +1473 -0
  76. solidworks_mcp/tools/input_compat.py +17 -0
  77. solidworks_mcp/tools/macro_recording.py +464 -0
  78. solidworks_mcp/tools/modeling.py +2360 -0
  79. solidworks_mcp/tools/sketching.py +1422 -0
  80. solidworks_mcp/tools/template_management.py +367 -0
  81. solidworks_mcp/tools/tool_boundary.py +280 -0
  82. solidworks_mcp/tools/vba_generation.py +1145 -0
  83. solidworks_mcp/ui/__init__.py +9 -0
  84. solidworks_mcp/ui/local_llm.py +725 -0
  85. solidworks_mcp/ui/routers/__init__.py +10 -0
  86. solidworks_mcp/ui/routers/checkpoint.py +24 -0
  87. solidworks_mcp/ui/routers/docs.py +47 -0
  88. solidworks_mcp/ui/routers/llm.py +83 -0
  89. solidworks_mcp/ui/routers/local_model.py +69 -0
  90. solidworks_mcp/ui/routers/model.py +75 -0
  91. solidworks_mcp/ui/routers/preview.py +38 -0
  92. solidworks_mcp/ui/routers/session.py +161 -0
  93. solidworks_mcp/ui/routers/viewer.py +154 -0
  94. solidworks_mcp/ui/schemas.py +502 -0
  95. solidworks_mcp/ui/server.py +298 -0
  96. solidworks_mcp/ui/service.py +481 -0
  97. solidworks_mcp/ui/services/__init__.py +136 -0
  98. solidworks_mcp/ui/services/_utils.py +958 -0
  99. solidworks_mcp/ui/services/checkpoint_service.py +1110 -0
  100. solidworks_mcp/ui/services/docs_service.py +282 -0
  101. solidworks_mcp/ui/services/llm_service.py +956 -0
  102. solidworks_mcp/ui/services/model_service.py +459 -0
  103. solidworks_mcp/ui/services/parameter_repair_service.py +314 -0
  104. solidworks_mcp/ui/services/preview_service.py +489 -0
  105. solidworks_mcp/ui/services/session_service.py +1389 -0
  106. solidworks_mcp/utils/__init__.py +9 -0
  107. solidworks_mcp/utils/feature_tree_classifier.py +342 -0
  108. solidworks_mcp/utils/logging.py +76 -0
  109. solidworks_mcp/utils/validation.py +77 -0
  110. solidworks_mcp/version.py +8 -0
  111. solidworks_mcp_python-1.5.0.dist-info/METADATA +410 -0
  112. solidworks_mcp_python-1.5.0.dist-info/RECORD +115 -0
  113. solidworks_mcp_python-1.5.0.dist-info/WHEEL +4 -0
  114. solidworks_mcp_python-1.5.0.dist-info/entry_points.txt +3 -0
  115. 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