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,10 @@
1
+ """FastAPI router for the ``routers`` sub-package.
2
+
3
+ Import individual routers and include them in the FastAPI app:
4
+
5
+ .. code-block:: python
6
+
7
+ from solidworks_mcp.ui.routers import session, model, preview, llm, checkpoint, docs, viewer
8
+ app.include_router(session.router)
9
+ ...
10
+ """
@@ -0,0 +1,24 @@
1
+ """Checkpoint execution route for the Prefab CAD dashboard."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from fastapi import APIRouter
8
+ from pydantic import BaseModel
9
+
10
+ from ..services import DEFAULT_SESSION_ID, execute_next_checkpoint
11
+
12
+ router = APIRouter()
13
+
14
+
15
+ class CheckpointExecuteRequest(BaseModel):
16
+ """Request payload for checkpoint execution."""
17
+
18
+ session_id: str = DEFAULT_SESSION_ID
19
+
20
+
21
+ @router.post("/api/ui/checkpoints/execute-next")
22
+ async def execute_checkpoint(payload: CheckpointExecuteRequest) -> dict[str, Any]:
23
+ """Execute the next pending design checkpoint against the SolidWorks adapter."""
24
+ return await execute_next_checkpoint(payload.session_id)
@@ -0,0 +1,47 @@
1
+ """Docs context and RAG ingestion routes for the Prefab CAD dashboard."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from fastapi import APIRouter
8
+ from pydantic import BaseModel
9
+
10
+ from ..services import DEFAULT_SESSION_ID, fetch_docs_context, ingest_reference_source
11
+
12
+ router = APIRouter()
13
+
14
+
15
+ class DocsContextRequest(BaseModel):
16
+ """Request payload for docs-context refresh."""
17
+
18
+ session_id: str = DEFAULT_SESSION_ID
19
+ query: str = "SolidWorks MCP endpoints"
20
+
21
+
22
+ class RagIngestRequest(BaseModel):
23
+ """Request payload for BYO retrieval ingestion from a local path or URL."""
24
+
25
+ session_id: str = DEFAULT_SESSION_ID
26
+ source_path: str
27
+ namespace: str = "engineering-reference"
28
+ chunk_size: int = 1200
29
+ overlap: int = 200
30
+
31
+
32
+ @router.post("/api/ui/docs/context")
33
+ async def docs_context(payload: DocsContextRequest) -> dict[str, Any]:
34
+ """Fetch and cache docs text from the /docs endpoint."""
35
+ return fetch_docs_context(payload.session_id, docs_query=payload.query)
36
+
37
+
38
+ @router.post("/api/ui/rag/ingest")
39
+ async def rag_ingest(payload: RagIngestRequest) -> dict[str, Any]:
40
+ """Ingest a local file or URL into the local retrieval index."""
41
+ return ingest_reference_source(
42
+ payload.session_id,
43
+ source_path=payload.source_path,
44
+ namespace=payload.namespace,
45
+ chunk_size=payload.chunk_size,
46
+ overlap=payload.overlap,
47
+ )
@@ -0,0 +1,83 @@
1
+ """LLM clarify / inspect / go-orchestration routes for the Prefab CAD dashboard."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from fastapi import APIRouter
8
+ from pydantic import BaseModel
9
+
10
+ from ...agents.history_db import get_design_session
11
+ from ..services import (
12
+ DEFAULT_SESSION_ID,
13
+ DEFAULT_USER_GOAL,
14
+ inspect_family,
15
+ request_clarifications,
16
+ run_go_orchestration,
17
+ )
18
+
19
+ router = APIRouter()
20
+
21
+
22
+ def _resolve_user_goal(session_id: str, user_goal: str) -> str:
23
+ """Prefer the approved session goal when the client sends the default placeholder."""
24
+
25
+ if user_goal and user_goal != DEFAULT_USER_GOAL:
26
+ return user_goal
27
+
28
+ session_row = get_design_session(session_id) or {}
29
+ return str(session_row.get("user_goal") or DEFAULT_USER_GOAL)
30
+
31
+
32
+ class ClarifyWithAnswerRequest(BaseModel):
33
+ """Request payload for clarify that includes the user's typed answers."""
34
+
35
+ session_id: str = DEFAULT_SESSION_ID
36
+ user_goal: str = DEFAULT_USER_GOAL
37
+ user_answer: str = ""
38
+
39
+
40
+ class GoOrchestrationRequest(BaseModel):
41
+ """Request payload for global Go orchestration action."""
42
+
43
+ session_id: str = DEFAULT_SESSION_ID
44
+ user_goal: str = DEFAULT_USER_GOAL
45
+ assumptions_text: str | None = None
46
+ user_answer: str = ""
47
+
48
+
49
+ class FamilyInspectRequest(BaseModel):
50
+ """Request payload for design family classification."""
51
+
52
+ session_id: str = DEFAULT_SESSION_ID
53
+ user_goal: str = DEFAULT_USER_GOAL
54
+
55
+
56
+ @router.post("/api/ui/clarify")
57
+ async def clarify(payload: ClarifyWithAnswerRequest) -> dict[str, Any]:
58
+ """Call the LLM to generate clarifying questions for the design goal."""
59
+ return await request_clarifications(
60
+ payload.session_id,
61
+ user_goal=_resolve_user_goal(payload.session_id, payload.user_goal),
62
+ user_answer=payload.user_answer,
63
+ )
64
+
65
+
66
+ @router.post("/api/ui/family/inspect")
67
+ async def family_inspect(payload: FamilyInspectRequest) -> dict[str, Any]:
68
+ """Call the LLM to classify the design family from the current goal."""
69
+ return await inspect_family(
70
+ payload.session_id,
71
+ user_goal=_resolve_user_goal(payload.session_id, payload.user_goal),
72
+ )
73
+
74
+
75
+ @router.post("/api/ui/orchestrate/go")
76
+ async def orchestrate_go(payload: GoOrchestrationRequest) -> dict[str, Any]:
77
+ """Run the full Go orchestration pipeline (clarify → inspect → plan checkpoints)."""
78
+ return await run_go_orchestration(
79
+ payload.session_id,
80
+ user_goal=_resolve_user_goal(payload.session_id, payload.user_goal),
81
+ assumptions_text=payload.assumptions_text,
82
+ user_answer=payload.user_answer,
83
+ )
@@ -0,0 +1,69 @@
1
+ """Local Ollama model probe / pull / query routes for the Prefab CAD dashboard."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from fastapi import APIRouter
6
+
7
+ from ..local_llm import (
8
+ LocalAgentResult,
9
+ LocalModelProbeResult,
10
+ LocalModelPullRequest,
11
+ LocalModelPullResult,
12
+ LocalModelQueryRequest,
13
+ )
14
+
15
+ router = APIRouter()
16
+
17
+
18
+ @router.get("/api/ui/local-model/probe")
19
+ async def probe_local_model_endpoint() -> LocalModelProbeResult:
20
+ """Probe for a running Ollama server and recommend the best Gemma model tier."""
21
+ from ..local_llm import probe_local_model # noqa: PLC0415
22
+
23
+ return await probe_local_model()
24
+
25
+
26
+ @router.post("/api/ui/local-model/pull")
27
+ async def pull_local_model_endpoint(
28
+ payload: LocalModelPullRequest,
29
+ ) -> LocalModelPullResult:
30
+ """Trigger an Ollama pull for the specified model name."""
31
+ from ..local_llm import pull_ollama_model # noqa: PLC0415
32
+
33
+ return await pull_ollama_model(model=payload.model, endpoint=payload.endpoint)
34
+
35
+
36
+ @router.post("/api/ui/local-model/query")
37
+ async def query_local_model_endpoint(
38
+ payload: LocalModelQueryRequest,
39
+ ) -> LocalAgentResult:
40
+ """Run a free-form prompt against the local Ollama model."""
41
+ from pydantic import BaseModel as _BaseModel
42
+
43
+ from ..local_llm import LocalLLMConfig, run_local_agent # noqa: PLC0415
44
+
45
+ class _FreeFormResponse(_BaseModel):
46
+ text: str
47
+
48
+ config = LocalLLMConfig.from_env()
49
+ if payload.endpoint:
50
+ config = config.model_copy(
51
+ update={
52
+ "endpoint": payload.endpoint,
53
+ "openai_endpoint": f"{payload.endpoint}/v1",
54
+ }
55
+ )
56
+ if payload.model:
57
+ service_model = (
58
+ payload.model
59
+ if payload.model.startswith("local:")
60
+ else f"local:{payload.model}"
61
+ )
62
+ config = config.model_copy(update={"service_model": service_model})
63
+
64
+ return await run_local_agent(
65
+ system_prompt=payload.system_prompt,
66
+ user_prompt=payload.prompt,
67
+ result_type=_FreeFormResponse,
68
+ config=config,
69
+ )
@@ -0,0 +1,75 @@
1
+ """Model open/connect routes for the Prefab CAD dashboard."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from fastapi import APIRouter
8
+ from pydantic import BaseModel, field_validator
9
+
10
+ from ..services import (
11
+ DEFAULT_SESSION_ID,
12
+ connect_target_model,
13
+ open_target_model,
14
+ )
15
+
16
+ router = APIRouter()
17
+
18
+
19
+ class UploadedFilePayload(BaseModel):
20
+ """Browser-uploaded file payload returned by Prefab's OpenFilePicker action."""
21
+
22
+ name: str
23
+ size: int
24
+ type: str
25
+ data: str
26
+
27
+
28
+ class ConnectTargetModelRequest(BaseModel):
29
+ """Request payload for attaching an active SolidWorks target model."""
30
+
31
+ session_id: str = DEFAULT_SESSION_ID
32
+ model_path: str | None = None
33
+ uploaded_files: list[UploadedFilePayload] | None = None
34
+ feature_target_text: str | None = None
35
+
36
+ @field_validator("uploaded_files", mode="before")
37
+ @classmethod
38
+ def _coerce_empty_uploaded_files(cls, v: object) -> object:
39
+ """Coerce empty string or empty list to None so Pydantic accepts it."""
40
+ if v == "" or v == []:
41
+ return None
42
+ return v
43
+
44
+
45
+ class OpenTargetModelRequest(BaseModel):
46
+ """Request payload for opening a target model by file path."""
47
+
48
+ session_id: str = DEFAULT_SESSION_ID
49
+ model_path: str | None = None
50
+ feature_target_text: str | None = None
51
+
52
+
53
+ @router.post("/api/ui/model/connect")
54
+ async def connect_model(payload: ConnectTargetModelRequest) -> dict[str, Any]:
55
+ """Attach a target SolidWorks document and derive grounded feature-tree context."""
56
+ return await connect_target_model(
57
+ payload.session_id,
58
+ model_path=payload.model_path,
59
+ uploaded_files=(
60
+ [f.model_dump() for f in payload.uploaded_files]
61
+ if payload.uploaded_files
62
+ else None
63
+ ),
64
+ feature_target_text=payload.feature_target_text,
65
+ )
66
+
67
+
68
+ @router.post("/api/ui/model/open")
69
+ async def open_model(payload: OpenTargetModelRequest) -> dict[str, Any]:
70
+ """Open a SolidWorks model by path and reflect its feature tree into the session."""
71
+ return await open_target_model(
72
+ payload.session_id,
73
+ model_path=payload.model_path,
74
+ feature_target_text=payload.feature_target_text,
75
+ )
@@ -0,0 +1,38 @@
1
+ """Preview and feature-highlight routes for the Prefab CAD dashboard."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from fastapi import APIRouter
8
+ from pydantic import BaseModel
9
+
10
+ from ..services import DEFAULT_SESSION_ID, highlight_feature, refresh_preview
11
+
12
+ router = APIRouter()
13
+
14
+
15
+ class PreviewRefreshRequest(BaseModel):
16
+ """Request payload for preview refresh requests."""
17
+
18
+ session_id: str = DEFAULT_SESSION_ID
19
+ orientation: str = "current"
20
+
21
+
22
+ class FeatureSelectRequest(BaseModel):
23
+ """Request payload for highlighting a named feature in the SolidWorks model tree."""
24
+
25
+ session_id: str = DEFAULT_SESSION_ID
26
+ feature_name: str
27
+
28
+
29
+ @router.post("/api/ui/preview/refresh")
30
+ async def preview_refresh(payload: PreviewRefreshRequest) -> dict[str, Any]:
31
+ """Export the current SolidWorks viewport to a PNG/GLB preview."""
32
+ return await refresh_preview(payload.session_id, orientation=payload.orientation)
33
+
34
+
35
+ @router.post("/api/ui/feature/select")
36
+ async def feature_select(payload: FeatureSelectRequest) -> dict[str, Any]:
37
+ """Select and highlight a named feature in the active SolidWorks model."""
38
+ return await highlight_feature(payload.session_id, payload.feature_name)
@@ -0,0 +1,161 @@
1
+ """Session management routes for the Prefab CAD dashboard.
2
+
3
+ Covers: state hydration, debug snapshot, brief approval, preferences,
4
+ workflow selection, notes, context save/load, family accept, and manual-sync reconcile.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any
10
+
11
+ from fastapi import APIRouter, Query
12
+ from pydantic import BaseModel
13
+
14
+ from ..services import (
15
+ DEFAULT_API_ORIGIN,
16
+ DEFAULT_SESSION_ID,
17
+ DEFAULT_USER_GOAL,
18
+ accept_family_choice,
19
+ approve_design_brief,
20
+ build_dashboard_state,
21
+ build_dashboard_trace_payload,
22
+ load_session_context,
23
+ reconcile_manual_edits,
24
+ save_session_context,
25
+ select_workflow_mode,
26
+ update_session_notes,
27
+ update_ui_preferences,
28
+ )
29
+
30
+ router = APIRouter()
31
+
32
+
33
+ # ---------------------------------------------------------------------------
34
+ # Request models
35
+ # ---------------------------------------------------------------------------
36
+
37
+
38
+ class SessionRequest(BaseModel):
39
+ """Base request payload containing session scope."""
40
+
41
+ session_id: str = DEFAULT_SESSION_ID
42
+
43
+
44
+ class GoalRequest(SessionRequest):
45
+ """Request payload for endpoints that operate on the current design goal."""
46
+
47
+ user_goal: str = DEFAULT_USER_GOAL
48
+
49
+
50
+ class FamilyAcceptRequest(SessionRequest):
51
+ """Request payload for family acceptance."""
52
+
53
+ family: str | None = None
54
+
55
+
56
+ class PreferencesUpdateRequest(SessionRequest):
57
+ """Request payload for assumptions and model preference updates."""
58
+
59
+ assumptions_text: str
60
+ model_provider: str = "github"
61
+ model_profile: str = "balanced"
62
+ model_name: str | None = None
63
+ local_endpoint: str | None = None
64
+
65
+
66
+ class WorkflowSelectionRequest(SessionRequest):
67
+ """Request payload for selecting the onboarding workflow branch."""
68
+
69
+ workflow_mode: str
70
+
71
+
72
+ class NotesUpdateRequest(SessionRequest):
73
+ """Request payload for saving free-form engineering notes."""
74
+
75
+ notes_text: str = ""
76
+
77
+
78
+ class ContextSaveRequest(SessionRequest):
79
+ """Request payload for saving context to a plain JSON snapshot."""
80
+
81
+ context_name: str | None = None
82
+
83
+
84
+ class ContextLoadRequest(SessionRequest):
85
+ """Request payload for loading context snapshot from plain JSON."""
86
+
87
+ context_file: str | None = None
88
+
89
+
90
+ # ---------------------------------------------------------------------------
91
+ # Routes
92
+ # ---------------------------------------------------------------------------
93
+
94
+
95
+ @router.get("/api/ui/state")
96
+ async def get_state(session_id: str = Query(DEFAULT_SESSION_ID)) -> dict[str, Any]:
97
+ """Hydrate UI state from active session database."""
98
+ return build_dashboard_state(session_id, api_origin=DEFAULT_API_ORIGIN)
99
+
100
+
101
+ @router.get("/api/ui/debug/session")
102
+ async def get_debug_session(
103
+ session_id: str = Query(DEFAULT_SESSION_ID),
104
+ ) -> dict[str, Any]:
105
+ """Return a verbose debug snapshot for the active UI session."""
106
+ return build_dashboard_trace_payload(session_id, api_origin=DEFAULT_API_ORIGIN)
107
+
108
+
109
+ @router.post("/api/ui/brief/approve")
110
+ async def approve_brief(payload: GoalRequest) -> dict[str, Any]:
111
+ """Accept the user-provided design goal."""
112
+ return approve_design_brief(payload.session_id, payload.user_goal)
113
+
114
+
115
+ @router.post("/api/ui/preferences/update")
116
+ async def update_preferences(payload: PreferencesUpdateRequest) -> dict[str, Any]:
117
+ """Persist assumptions and provider/model preferences in session metadata."""
118
+ return update_ui_preferences(
119
+ payload.session_id,
120
+ assumptions_text=payload.assumptions_text,
121
+ model_provider=payload.model_provider,
122
+ model_profile=payload.model_profile,
123
+ model_name=payload.model_name,
124
+ local_endpoint=payload.local_endpoint,
125
+ )
126
+
127
+
128
+ @router.post("/api/ui/workflow/select")
129
+ async def update_workflow_mode(payload: WorkflowSelectionRequest) -> dict[str, Any]:
130
+ """Persist the workflow choice shown on the opening dashboard screen."""
131
+ return select_workflow_mode(payload.session_id, workflow_mode=payload.workflow_mode)
132
+
133
+
134
+ @router.post("/api/ui/family/accept")
135
+ async def accept_family(payload: FamilyAcceptRequest) -> dict[str, Any]:
136
+ """Accept the proposed design family classification."""
137
+ return accept_family_choice(payload.session_id, family=payload.family)
138
+
139
+
140
+ @router.post("/api/ui/notes/update")
141
+ async def update_notes(payload: NotesUpdateRequest) -> dict[str, Any]:
142
+ """Persist free-form engineering notes for the session."""
143
+ return update_session_notes(payload.session_id, notes_text=payload.notes_text)
144
+
145
+
146
+ @router.post("/api/ui/context/save")
147
+ async def context_save(payload: ContextSaveRequest) -> dict[str, Any]:
148
+ """Save current session context to a JSON snapshot file."""
149
+ return save_session_context(payload.session_id, context_name=payload.context_name)
150
+
151
+
152
+ @router.post("/api/ui/context/load")
153
+ async def context_load(payload: ContextLoadRequest) -> dict[str, Any]:
154
+ """Load a previously saved session context JSON snapshot."""
155
+ return load_session_context(payload.session_id, context_file=payload.context_file)
156
+
157
+
158
+ @router.post("/api/ui/manual-sync/reconcile")
159
+ async def reconcile_edits(payload: SessionRequest) -> dict[str, Any]:
160
+ """Detect manual edits via snapshot diff and apply forward delta."""
161
+ return reconcile_manual_edits(payload.session_id)
@@ -0,0 +1,154 @@
1
+ """Three.js 3D model viewer route for the Prefab CAD dashboard.
2
+
3
+ # TODO: move _VIEWER_HTML to a Jinja2 template file at templates/viewer.html
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from fastapi import APIRouter, Path
9
+ from fastapi.responses import HTMLResponse
10
+
11
+ router = APIRouter()
12
+
13
+ # ---------------------------------------------------------------------------
14
+ # Embedded Three.js viewer HTML
15
+ # ---------------------------------------------------------------------------
16
+ # TODO: move to templates/viewer.html and serve via Jinja2TemplateResponse
17
+ _VIEWER_HTML = """\
18
+ <!DOCTYPE html><html lang="en"><head><meta charset="UTF-8">
19
+ <meta name="viewport" content="width=device-width, initial-scale=1">
20
+ <title>3D Model Viewer</title>
21
+ <style>
22
+ * { margin: 0; padding: 0; box-sizing: border-box; }
23
+ body { background: #0f172a; color: #94a3b8; font-family: system-ui, sans-serif; overflow: hidden; }
24
+ #wrap { width: 100vw; height: 100vh; }
25
+ #overlay { position: fixed; inset: 0; display: flex; flex-direction: column;
26
+ align-items: center; justify-content: center; gap: 10px; pointer-events: none; }
27
+ #status { font-size: 13px; text-align: center; max-width: 300px; line-height: 1.6; }
28
+ #hint { position: fixed; bottom: 10px; left: 50%; transform: translateX(-50%);
29
+ font-size: 11px; opacity: 0.35; user-select: none; }
30
+ #fmt-badge { position: fixed; top: 8px; right: 10px; font-size: 10px;
31
+ opacity: 0.4; letter-spacing: 0.05em; user-select: none; }
32
+ </style></head><body>
33
+ <div id="wrap"></div>
34
+ <div id="overlay">
35
+ <div id="icon" style="font-size:28px">&#9203;</div>
36
+ <div id="status">Loading 3D model&#8230;</div>
37
+ </div>
38
+ <div id="hint">Drag to rotate &middot; Scroll to zoom &middot; Right-drag to pan</div>
39
+ <div id="fmt-badge"></div>
40
+ <script type="importmap">{"imports": {
41
+ "three": "https://cdn.jsdelivr.net/npm/three@0.165.0/build/three.module.js",
42
+ "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.165.0/examples/jsm/"
43
+ }}</script>
44
+ <script type="module">
45
+ import * as THREE from 'three';
46
+ import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
47
+ import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
48
+ import { STLLoader } from 'three/addons/loaders/STLLoader.js';
49
+
50
+ const params = new URLSearchParams(location.search);
51
+ const pathParts = location.pathname.split('/').filter(Boolean);
52
+ const pathSessionId = pathParts[pathParts.length - 1] || 'prefab-dashboard';
53
+ const sessionId = params.get('session_id') || pathSessionId;
54
+ const ts = params.get('t') || '0';
55
+ const fmt = params.get('fmt') || 'stl';
56
+
57
+ const renderer = new THREE.WebGLRenderer({ antialias: true });
58
+ renderer.setPixelRatio(window.devicePixelRatio);
59
+ renderer.outputColorSpace = THREE.SRGBColorSpace;
60
+ renderer.setSize(window.innerWidth, window.innerHeight);
61
+ document.getElementById('wrap').appendChild(renderer.domElement);
62
+
63
+ const scene = new THREE.Scene();
64
+ scene.background = new THREE.Color(0x0f172a);
65
+
66
+ const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.01, 100000);
67
+ camera.position.set(0, 100, 250);
68
+
69
+ const controls = new OrbitControls(camera, renderer.domElement);
70
+ controls.enableDamping = true;
71
+ controls.dampingFactor = 0.06;
72
+
73
+ scene.add(new THREE.AmbientLight(0xffffff, 0.8));
74
+ const dirLight = new THREE.DirectionalLight(0xffffff, 1.2);
75
+ dirLight.position.set(1, 2, 1.5);
76
+ scene.add(dirLight);
77
+ const fillLight = new THREE.DirectionalLight(0x8ab4f8, 0.4);
78
+ fillLight.position.set(-1, -1, -1);
79
+ scene.add(fillLight);
80
+
81
+ const stlMaterial = new THREE.MeshPhongMaterial({
82
+ color: 0x3b82f6, specular: 0x1e3a5f, shininess: 60, side: THREE.DoubleSide
83
+ });
84
+
85
+ function fitCamera(object) {
86
+ const box = new THREE.Box3().setFromObject(object);
87
+ const size = box.getSize(new THREE.Vector3());
88
+ const center = box.getCenter(new THREE.Vector3());
89
+ const maxDim = Math.max(size.x, size.y, size.z) || 100;
90
+ camera.position.set(center.x, center.y + maxDim * 0.6, center.z + maxDim * 2);
91
+ camera.near = maxDim * 0.001;
92
+ camera.far = maxDim * 200;
93
+ camera.updateProjectionMatrix();
94
+ controls.target.copy(center);
95
+ controls.update();
96
+ }
97
+
98
+ function hideOverlay() { document.getElementById('overlay').style.display = 'none'; }
99
+ function showError(msg) {
100
+ document.getElementById('icon').textContent = '( )';
101
+ document.getElementById('status').textContent = msg;
102
+ }
103
+ function onProgress(p) {
104
+ const pct = p.total ? Math.round(p.loaded / p.total * 100) : 0;
105
+ document.getElementById('status').textContent = 'Loading\\u2026 ' + pct + '%';
106
+ }
107
+
108
+ if (fmt === 'glb') {
109
+ document.getElementById('fmt-badge').textContent = 'GLB';
110
+ const glbUrl = location.origin + '/previews/' + sessionId + '.glb?_t=' + ts;
111
+ new GLTFLoader().load(glbUrl, (gltf) => {
112
+ hideOverlay();
113
+ const model = gltf.scene;
114
+ model.traverse((node) => {
115
+ if (node.isMesh && node.material) {
116
+ const mats = Array.isArray(node.material) ? node.material : [node.material];
117
+ mats.forEach((m) => { m.side = THREE.DoubleSide; });
118
+ }
119
+ });
120
+ scene.add(model);
121
+ fitCamera(model);
122
+ }, onProgress, () => showError('No 3D model file yet. Attach a SolidWorks model, then click Refresh 3D View.'));
123
+ } else if (fmt === 'stl') {
124
+ document.getElementById('fmt-badge').textContent = 'STL';
125
+ const stlUrl = location.origin + '/previews/' + sessionId + '.stl?_t=' + ts;
126
+ new STLLoader().load(stlUrl, (geometry) => {
127
+ hideOverlay();
128
+ geometry.computeBoundingBox();
129
+ geometry.center();
130
+ geometry.computeVertexNormals();
131
+ const mesh = new THREE.Mesh(geometry, stlMaterial);
132
+ scene.add(mesh);
133
+ fitCamera(mesh);
134
+ }, onProgress, () => showError('No 3D model file yet. Attach a SolidWorks model, then click Refresh 3D View.'));
135
+ } else {
136
+ showError('No 3D model file yet. Attach a SolidWorks model, then click Refresh 3D View.');
137
+ }
138
+
139
+ window.addEventListener('resize', () => {
140
+ camera.aspect = window.innerWidth / window.innerHeight;
141
+ camera.updateProjectionMatrix();
142
+ renderer.setSize(window.innerWidth, window.innerHeight);
143
+ });
144
+ (function animate() { requestAnimationFrame(animate); controls.update(); renderer.render(scene, camera); })();
145
+ </script></body></html>
146
+ """
147
+
148
+
149
+ @router.get("/api/ui/viewer/{session_id}", response_class=HTMLResponse)
150
+ async def get_viewer(
151
+ session_id: str = Path(description="Session identifier for 3D model file routing"),
152
+ ) -> HTMLResponse:
153
+ """Serve the embedded Three.js 3D model viewer page."""
154
+ return HTMLResponse(content=_VIEWER_HTML, media_type="text/html")