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,956 @@
1
+ """LLM orchestration service for the Prefab CAD assistant dashboard.
2
+
3
+ Wraps all pydantic-ai calls for clarification, family inspection, and Go orchestration.
4
+ This module is the only place that imports pydantic-ai for dashboard orchestration.
5
+
6
+ Security note:
7
+ ``_ensure_provider_credentials`` is environment-variable only and does not shell
8
+ out to external credential helpers.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import inspect as _inspect
14
+ import json
15
+ import os
16
+ import re
17
+ from pathlib import Path
18
+ from typing import Any, Literal
19
+
20
+ from loguru import logger
21
+ from pydantic import BaseModel, Field
22
+
23
+ from ...agents.history_db import (
24
+ get_design_session,
25
+ insert_evidence_link,
26
+ insert_tool_call_record,
27
+ replace_plan_checkpoints,
28
+ )
29
+ from ._utils import (
30
+ DEFAULT_API_ORIGIN,
31
+ DEFAULT_USER_GOAL,
32
+ merge_metadata,
33
+ normalize_model_name_for_provider,
34
+ persist_ui_action,
35
+ sanitize_ui_text,
36
+ )
37
+
38
+ # ---------------------------------------------------------------------------
39
+ # Optional pydantic-ai imports — library is an optional dependency.
40
+ # ---------------------------------------------------------------------------
41
+ try:
42
+ from pydantic_ai import Agent
43
+ from pydantic_ai.models.openai import OpenAIChatModel
44
+ from pydantic_ai.providers.openai import OpenAIProvider
45
+
46
+ try:
47
+ from pydantic_ai.mcp import MCPServerStreamableHTTP
48
+ except ImportError:
49
+ MCPServerStreamableHTTP = None # type: ignore[assignment]
50
+
51
+ try:
52
+ from pydantic_ai import RecoverableFailure
53
+ except ImportError:
54
+ # Older pydantic-ai builds expose it at a different path.
55
+ try:
56
+ from pydantic_ai.result import RecoverableFailure # type: ignore[no-redef]
57
+ except ImportError:
58
+ # Newer pydantic-ai builds may not expose RecoverableFailure.
59
+ # Keep Agent support enabled and use a local schema fallback.
60
+ class RecoverableFailure(BaseModel): # type: ignore[no-redef]
61
+ """Fallback recoverable-failure schema for pydantic-ai compatibility."""
62
+
63
+ explanation: str = ""
64
+ remediation_steps: list[str] = Field(default_factory=list)
65
+ retry_focus: str = ""
66
+ should_retry: bool = False
67
+ except ImportError:
68
+ Agent = None # type: ignore[assignment]
69
+ OpenAIChatModel = None # type: ignore[assignment]
70
+ OpenAIProvider = None # type: ignore[assignment]
71
+ MCPServerStreamableHTTP = None # type: ignore[assignment]
72
+
73
+ class RecoverableFailure(BaseModel): # type: ignore[no-redef]
74
+ """Fallback stub used when pydantic-ai is not installed."""
75
+
76
+ explanation: str = ""
77
+ remediation_steps: list[str] = Field(default_factory=list)
78
+ retry_focus: str = ""
79
+ should_retry: bool = False
80
+
81
+
82
+ # ---------------------------------------------------------------------------
83
+ # LLM response schemas (moved from service.py to avoid duplicate definitions)
84
+ # ---------------------------------------------------------------------------
85
+
86
+
87
+ class ClarificationResponse(BaseModel):
88
+ """LLM response for goal clarification.
89
+
90
+ Attributes:
91
+ normalized_brief: Concise manufacturing-ready description of the design goal.
92
+ questions: Follow-up questions that unblock the next modelling step.
93
+ """
94
+
95
+ normalized_brief: str = Field(min_length=10)
96
+ questions: list[str] = Field(default_factory=list)
97
+
98
+
99
+ class CheckpointCandidate(BaseModel):
100
+ """One suggested execution checkpoint.
101
+
102
+ Attributes:
103
+ title: Short human-readable label.
104
+ allowed_tools: MCP tool names allowed at this checkpoint.
105
+ rationale: Reasoning for including this step.
106
+ execution: Optional structured execution hints used by checkpoint runner.
107
+ """
108
+
109
+ title: str = Field(min_length=3)
110
+ allowed_tools: list[str] = Field(min_length=1)
111
+ rationale: str = Field(min_length=5)
112
+ execution: dict[str, Any] = Field(default_factory=dict)
113
+
114
+
115
+ class FamilyInspection(BaseModel):
116
+ """LLM response for feature-family classification.
117
+
118
+ Attributes:
119
+ family: Detected SolidWorks feature family name.
120
+ confidence: Model confidence level.
121
+ evidence: Supporting evidence lines.
122
+ warnings: Contradictory or low-confidence warnings.
123
+ checkpoints: Suggested checkpoint plan for human review.
124
+ """
125
+
126
+ family: str = Field(min_length=3)
127
+ confidence: Literal["low", "medium", "high"]
128
+ evidence: list[str] = Field(default_factory=list)
129
+ warnings: list[str] = Field(default_factory=list)
130
+ checkpoints: list[CheckpointCandidate] = Field(default_factory=list)
131
+
132
+
133
+ def _extract_explicit_feature_order(user_goal: str) -> list[str]:
134
+ """Extract feature order tokens from goals that include an explicit ``A -> B -> C`` sequence."""
135
+ if "->" not in (user_goal or ""):
136
+ return []
137
+
138
+ pattern = r"([A-Za-z][A-Za-z0-9_-]*(?:\s*->\s*[A-Za-z][A-Za-z0-9_-]*){1,9})"
139
+ match = re.search(pattern, user_goal)
140
+ if not match:
141
+ return []
142
+
143
+ sequence = match.group(1)
144
+ ordered = [part.strip() for part in sequence.split("->") if part.strip()]
145
+ return ordered
146
+
147
+
148
+ def _extract_blind_depth_mm(user_goal: str) -> float | None:
149
+ """Extract blind-depth numeric value from phrases like ``blind 10.0 mm``."""
150
+ match = re.search(r"blind\s+([0-9]+(?:\.[0-9]+)?)\s*mm", user_goal, re.IGNORECASE)
151
+ if not match:
152
+ return None
153
+ try:
154
+ return float(match.group(1))
155
+ except ValueError:
156
+ return None
157
+
158
+
159
+ def _tools_for_feature_name(feature_name: str, position: int) -> list[str]:
160
+ """Map a feature token to an executable tool subset used by checkpoint execution."""
161
+ lower = feature_name.strip().lower()
162
+ if "sketch" in lower:
163
+ # First sketch should establish profile edges; later sketches usually add detail geometry.
164
+ return (
165
+ ["create_sketch", "add_line", "exit_sketch"]
166
+ if position == 1
167
+ else ["create_sketch", "add_circle", "exit_sketch"]
168
+ )
169
+ if "cut" in lower:
170
+ return ["create_cut"]
171
+ if "revolve" in lower:
172
+ return ["create_revolve"]
173
+ if "extrude" in lower:
174
+ return ["create_extrusion"]
175
+ return ["analyze_geometry"]
176
+
177
+
178
+ def _family_for_feature_order(ordered_features: list[str], current_family: str) -> str:
179
+ """Infer family from explicit feature sequence, preferring deterministic extrusion flows."""
180
+ lowered = [item.strip().lower() for item in ordered_features]
181
+ if any("assembly" in item for item in lowered):
182
+ return "assembly"
183
+ if any("revolve" in item for item in lowered):
184
+ return "revolve"
185
+ if any(token in item for item in lowered for token in ("extrude", "cut", "sketch")):
186
+ return "extrude"
187
+ return current_family
188
+
189
+
190
+ def _coerce_explicit_feature_order_plan(
191
+ user_goal: str,
192
+ result: FamilyInspection,
193
+ ) -> FamilyInspection:
194
+ """Coerce checkpoints to an explicit feature sequence when the goal provides one."""
195
+ ordered_features = _extract_explicit_feature_order(user_goal)
196
+ if len(ordered_features) < 2:
197
+ return result
198
+
199
+ blind_depth_mm = _extract_blind_depth_mm(user_goal)
200
+ checkpoints: list[CheckpointCandidate] = []
201
+ for index, name in enumerate(ordered_features, start=1):
202
+ lower = name.strip().lower()
203
+ execution: dict[str, Any] = {
204
+ "feature_name": name,
205
+ "step_index": index,
206
+ "goal": f"Create {name}",
207
+ }
208
+
209
+ if "sketch" in lower and index == 1:
210
+ execution.update(
211
+ {
212
+ "sketch_plane": "Top",
213
+ "profile": "closed_rect",
214
+ "rect_width_mm": 40.0,
215
+ "rect_height_mm": 24.0,
216
+ }
217
+ )
218
+ elif "sketch" in lower:
219
+ execution.update(
220
+ {
221
+ "sketch_name": name,
222
+ "sketch_plane": "Top",
223
+ "profile": "circle",
224
+ "circle_center_mm": [20.0, 12.0],
225
+ "circle_radius_mm": 4.0,
226
+ }
227
+ )
228
+
229
+ if "extrude" in lower:
230
+ execution.update(
231
+ {
232
+ "depth_mm": 10.0,
233
+ "prepare_profile": "closed_rect",
234
+ "sketch_plane": "Top",
235
+ "rect_width_mm": 40.0,
236
+ "rect_height_mm": 24.0,
237
+ }
238
+ )
239
+ if "cut" in lower and blind_depth_mm is not None:
240
+ prior_sketch = next(
241
+ (
242
+ feature
243
+ for feature in reversed(ordered_features[: index - 1])
244
+ if "sketch" in feature.lower()
245
+ ),
246
+ "Sketch1",
247
+ )
248
+ execution.update(
249
+ {
250
+ "depth_mm": blind_depth_mm,
251
+ "sketch_name": prior_sketch,
252
+ "prepare_base_extrusion": True,
253
+ "prepare_profile": "circle",
254
+ "sketch_plane": "Top",
255
+ "circle_center_mm": [20.0, 12.0],
256
+ "circle_radius_mm": 4.0,
257
+ "base_depth_mm": 10.0,
258
+ }
259
+ )
260
+
261
+ checkpoints.append(
262
+ CheckpointCandidate(
263
+ title=f"Create {name}",
264
+ allowed_tools=_tools_for_feature_name(name, index),
265
+ rationale=(
266
+ f"Follow the explicit feature order from the design goal: step {index} is {name}."
267
+ ),
268
+ execution=execution,
269
+ )
270
+ )
271
+
272
+ return result.model_copy(
273
+ update={
274
+ "family": _family_for_feature_order(ordered_features, result.family),
275
+ "confidence": "high" if len(ordered_features) >= 3 else result.confidence,
276
+ "checkpoints": checkpoints,
277
+ "warnings": [
278
+ *result.warnings,
279
+ "Checkpoint plan constrained to the explicit feature-order sequence in the design goal.",
280
+ ],
281
+ }
282
+ )
283
+
284
+
285
+ # ---------------------------------------------------------------------------
286
+ # Internal helpers
287
+ # ---------------------------------------------------------------------------
288
+
289
+
290
+ def _resolve_model_name(explicit_model: str | None = None) -> str:
291
+ """Return the resolved model name, falling back to the environment variable.
292
+
293
+ Args:
294
+ explicit_model: Caller-supplied model string (may be ``None``).
295
+
296
+ Returns:
297
+ Model name string to pass to pydantic-ai.
298
+ """
299
+ return explicit_model or os.getenv("SOLIDWORKS_UI_MODEL", "github:openai/gpt-4.1")
300
+
301
+
302
+ def _ensure_provider_credentials(
303
+ model_name: str,
304
+ local_endpoint: str | None = None,
305
+ ) -> None:
306
+ """Verify that the required provider credential is present, raising otherwise.
307
+
308
+ Args:
309
+ model_name: Provider-qualified model name (e.g. ``"github:openai/gpt-4.1"``).
310
+ local_endpoint: Optional base URL for a local LLM server.
311
+
312
+ Raises:
313
+ RuntimeError: When the required credential is missing.
314
+ """
315
+ if model_name.startswith("github:"):
316
+ github_token = os.getenv("GITHUB_API_KEY") or os.getenv("GH_TOKEN")
317
+ if not github_token:
318
+ raise RuntimeError(
319
+ "Set GH_TOKEN or GITHUB_API_KEY with models:read scope before using the dashboard LLM actions."
320
+ )
321
+ os.environ.setdefault("GITHUB_API_KEY", github_token)
322
+ return
323
+
324
+ if model_name.startswith("openai:") and not os.getenv("OPENAI_API_KEY"):
325
+ raise RuntimeError("Set OPENAI_API_KEY before using OpenAI model routing.")
326
+
327
+ if model_name.startswith("anthropic:") and not os.getenv("ANTHROPIC_API_KEY"):
328
+ raise RuntimeError(
329
+ "Set ANTHROPIC_API_KEY before using Anthropic model routing."
330
+ )
331
+
332
+ if model_name.startswith("local:"):
333
+ # Endpoint defaults to http://127.0.0.1:11434/v1 inside _build_agent_model;
334
+ # no credential is needed for a local Ollama server.
335
+ pass
336
+
337
+
338
+ def _build_agent_model(model_name: str, local_endpoint: str | None = None) -> Any:
339
+ """Construct and return the pydantic-ai model object for the given name.
340
+
341
+ Args:
342
+ model_name: Provider-qualified model name.
343
+ local_endpoint: Base URL used when ``model_name`` starts with ``"local:"``.
344
+
345
+ Returns:
346
+ Either an ``OpenAIChatModel`` instance or the bare string model name.
347
+
348
+ Raises:
349
+ RuntimeError: When the local-model path is requested but pydantic-ai's OpenAI
350
+ provider is not installed.
351
+ """
352
+ if model_name.startswith("local:"):
353
+ if OpenAIChatModel is None or OpenAIProvider is None:
354
+ raise RuntimeError("pydantic-ai OpenAI provider support is not installed.")
355
+ resolved_endpoint = local_endpoint or os.getenv(
356
+ "SOLIDWORKS_UI_LOCAL_ENDPOINT", "http://127.0.0.1:11434/v1"
357
+ )
358
+ provider = OpenAIProvider(
359
+ base_url=resolved_endpoint,
360
+ api_key=os.getenv("LOCAL_OPENAI_API_KEY")
361
+ or os.getenv("OPENAI_API_KEY")
362
+ or "local",
363
+ )
364
+ return OpenAIChatModel(model_name.split(":", 1)[1], provider=provider)
365
+
366
+ if model_name.startswith("github:"):
367
+ if OpenAIChatModel is None:
368
+ raise RuntimeError("pydantic-ai OpenAI support is not installed.")
369
+ try:
370
+ from pydantic_ai.providers.github import GitHubProvider # noqa: PLC0415
371
+ except ImportError as exc:
372
+ raise RuntimeError(
373
+ "pydantic-ai GitHubProvider is not available in this environment."
374
+ ) from exc
375
+ gh_token = os.getenv("GITHUB_API_KEY") or os.getenv("GH_TOKEN")
376
+ provider = GitHubProvider(**({} if gh_token is None else {"api_key": gh_token}))
377
+ raw_model = model_name.split(":", 1)[1] # e.g. "openai/gpt-4.1"
378
+ return OpenAIChatModel(raw_model, provider=provider)
379
+
380
+ return model_name
381
+
382
+
383
+ async def _run_structured_agent(
384
+ *,
385
+ system_prompt: str,
386
+ user_prompt: str,
387
+ result_type: type[BaseModel],
388
+ model_name: str | None = None,
389
+ local_endpoint: str | None = None,
390
+ ) -> BaseModel | RecoverableFailure:
391
+ """Run a pydantic-ai agent and return a structured result or a RecoverableFailure.
392
+
393
+ Args:
394
+ system_prompt: System-level instructions for the agent.
395
+ user_prompt: User turn containing the planning request.
396
+ result_type: Pydantic model class expected as the structured output.
397
+ model_name: Optional provider-qualified model override.
398
+ local_endpoint: Optional base URL for local LLM servers.
399
+
400
+ Returns:
401
+ Either an instance of ``result_type`` or a ``RecoverableFailure`` describing
402
+ why the agent call failed and how to recover.
403
+ """
404
+ if Agent is None:
405
+ return RecoverableFailure(
406
+ explanation="pydantic_ai is not installed in this environment.",
407
+ remediation_steps=["Install project dependencies and retry."],
408
+ retry_focus="Install pydantic-ai and a supported provider.",
409
+ should_retry=False,
410
+ )
411
+
412
+ resolved_model = _resolve_model_name(model_name)
413
+ resolved_endpoint = local_endpoint or os.getenv(
414
+ "SOLIDWORKS_UI_LOCAL_ENDPOINT", "http://127.0.0.1:11434/v1"
415
+ )
416
+
417
+ mcp_tool_catalog = (
418
+ "## SOLIDWORKS MCP TOOL CATALOG\n"
419
+ "Use these tool names in checkpoint plans and rationale fields:\n"
420
+ " open_model, get_model_info, list_features(include_suppressed), get_mass_properties,\n"
421
+ " classify_feature_tree, create_sketch, add_line, add_arc, add_circle, add_rectangle,\n"
422
+ " create_extrusion, create_revolve, create_cut_extrude, export_image, export_step,\n"
423
+ " export_stl, select_feature, select_face_edges, add_fillet, add_chamfer,\n"
424
+ " rebuild_model, check_interference, analyze_geometry,\n"
425
+ " generate_vba_code, execute_macro\n"
426
+ "Prefer tools in the order listed above (inspect → classify → plan → execute → verify).\n"
427
+ "Do not invent tools not in this list.\n"
428
+ "A failed tool returns status 'error' with an error_type and a hint: follow the "
429
+ "hint (open a document, fix the argument, wait for the breaker) instead of "
430
+ "repeating the same call."
431
+ )
432
+ enriched_system_prompt = (
433
+ "## ROLE AND ORCHESTRATION AGENTS\n"
434
+ f"{system_prompt}\n\n"
435
+ "## AVAILABLE ORCHESTRATION AGENTS\n"
436
+ " - Feature-Tree Reconstruction agent: inspect → classify → delegate; safe checkpoint plans.\n"
437
+ " - Printer-Profile Tolerancing agent: converts printer/material inputs to explicit tolerance "
438
+ "ranges per feature type (press-fit, sliding, snap, hinge, clip).\n"
439
+ " - SolidWorks Research Validator: validates material, clearance, and build-volume facts.\n\n"
440
+ "## GUARDRAILS\n"
441
+ " - Use the SolidWorks MCP tool surface for actionable plans.\n"
442
+ " - Do not invent unavailable tools; prefer known MCP tool names.\n"
443
+ " - If confidence is low or the model is unavailable, propose inspection steps first.\n"
444
+ " - For sheet metal or advanced solid families, route to VBA-aware planning.\n"
445
+ " - Always include explicit tolerance/clearance values when manufacturing context is present."
446
+ )
447
+ enriched_user_prompt = f"## PLANNING REQUEST\n{user_prompt}\n\n{mcp_tool_catalog}"
448
+
449
+ mcp_server_url = os.getenv("SOLIDWORKS_MCP_URL", "http://127.0.0.1:8000/")
450
+ mcp_agent_tools = os.getenv("SOLIDWORKS_MCP_AGENT_TOOLS", "auto").lower()
451
+ toolsets: list[Any] = []
452
+ if mcp_agent_tools != "off" and MCPServerStreamableHTTP is not None:
453
+ mcp_kwargs: dict[str, Any] = {"tool_prefix": "sw"}
454
+ try:
455
+ params = _inspect.signature(MCPServerStreamableHTTP).parameters
456
+ if "include_instructions" in params:
457
+ mcp_kwargs["include_instructions"] = True
458
+ except (TypeError, ValueError):
459
+ pass
460
+ toolsets = [MCPServerStreamableHTTP(mcp_server_url, **mcp_kwargs)]
461
+
462
+ try:
463
+ _ensure_provider_credentials(resolved_model, resolved_endpoint)
464
+ configured_model = _build_agent_model(resolved_model, resolved_endpoint)
465
+ agent = Agent(
466
+ configured_model,
467
+ system_prompt=enriched_system_prompt,
468
+ output_type=[result_type, RecoverableFailure],
469
+ toolsets=toolsets if toolsets else None,
470
+ )
471
+ if toolsets:
472
+ try:
473
+ async with agent:
474
+ result = await agent.run(enriched_user_prompt)
475
+ except Exception:
476
+ logger.debug(
477
+ "MCP server at %s unreachable; falling back to planning-only agent run.",
478
+ mcp_server_url,
479
+ )
480
+ agent_fallback = Agent(
481
+ configured_model,
482
+ system_prompt=enriched_system_prompt,
483
+ output_type=[result_type, RecoverableFailure],
484
+ )
485
+ result = await agent_fallback.run(enriched_user_prompt)
486
+ else:
487
+ result = await agent.run(enriched_user_prompt)
488
+ except Exception as exc:
489
+ return RecoverableFailure(
490
+ explanation=f"Model routing failed: {exc}",
491
+ remediation_steps=[
492
+ "Open Model Controls and run Auto-Detect Local Model, or switch provider/model to a supported value.",
493
+ ],
494
+ retry_focus="Use a provider-qualified model name, then retry this action.",
495
+ should_retry=True,
496
+ )
497
+ payload = result.data if hasattr(result, "data") else result.output
498
+ if isinstance(payload, RecoverableFailure):
499
+ return payload
500
+ return (
501
+ payload
502
+ if isinstance(payload, result_type)
503
+ else result_type.model_validate(payload)
504
+ )
505
+
506
+
507
+ # ---------------------------------------------------------------------------
508
+ # Public API
509
+ # ---------------------------------------------------------------------------
510
+
511
+
512
+ async def request_clarifications(
513
+ session_id: str,
514
+ user_goal: str,
515
+ *,
516
+ user_answer: str = "",
517
+ db_path: Path | None = None,
518
+ model_name: str | None = None,
519
+ ) -> dict[str, Any]:
520
+ """Generate focused follow-up questions for the current design goal using LLM.
521
+
522
+ Args:
523
+ session_id: Dashboard session identifier.
524
+ user_goal: Free-text description of the design intent.
525
+ user_answer: Any previous answers the user has already provided.
526
+ db_path: Optional SQLite path override.
527
+ model_name: Optional provider-qualified model override.
528
+
529
+ Returns:
530
+ Full dashboard state payload.
531
+ """
532
+ from .session_service import ( # noqa: PLC0415
533
+ build_dashboard_state,
534
+ ensure_dashboard_session,
535
+ )
536
+
537
+ ensure_dashboard_session(session_id, user_goal=user_goal, db_path=db_path)
538
+ session_row = get_design_session(session_id, db_path=db_path) or {}
539
+ meta = _parse_json_blob(session_row.get("metadata_json"))
540
+ resolved_model = normalize_model_name_for_provider(
541
+ model_name or meta.get("model_name"),
542
+ provider=sanitize_ui_text(meta.get("model_provider"), "github"),
543
+ profile=sanitize_ui_text(meta.get("model_profile"), "balanced"),
544
+ )
545
+ resolved_endpoint = sanitize_ui_text(
546
+ meta.get("local_endpoint"),
547
+ os.getenv("SOLIDWORKS_UI_LOCAL_ENDPOINT", "http://127.0.0.1:11434/v1"),
548
+ )
549
+
550
+ answer_section = (
551
+ f"\n## USER ANSWERS / CLARIFICATIONS\n{user_answer}" if user_answer else ""
552
+ )
553
+ assumptions_text = sanitize_ui_text(
554
+ meta.get("assumptions_text"), "<none specified>"
555
+ )
556
+ prompt = (
557
+ "## TASK\n"
558
+ "Prepare a SolidWorks design brief using the Printer-Profile-Tolerancing skill.\n"
559
+ "Return a normalized_brief and at most three clarifying_questions that unblock the next modeling step.\n\n"
560
+ "## DESIGN GOAL\n"
561
+ f"{user_goal}\n\n"
562
+ "## MANUFACTURING ASSUMPTIONS\n"
563
+ f"{assumptions_text}\n\n"
564
+ "## MODEL CONTEXT\n"
565
+ f"Active model path : {meta.get('active_model_path', '') or '<none>'}\n"
566
+ f"Feature target refs: {meta.get('feature_target_text', '') or '<none>'}\n"
567
+ f"Reference corpus : {meta.get('rag_provenance_text', '') or '<none>'}"
568
+ f"{answer_section}\n\n"
569
+ "## OUTPUT CONTRACT\n"
570
+ "normalized_brief: concise paragraph with explicit dimensions/tolerances where known (≥10 chars).\n"
571
+ "questions : list of up to 3 highest-leverage questions that unblock modeling.\n"
572
+ " - Return an empty question list when the goal, assumptions, and user answers already provide enough detail to sketch and dimension the next feature.\n"
573
+ " - Include material/layer-height/nozzle values if missing from assumptions.\n"
574
+ " - Include critical fit/clearance targets if unspecified.\n"
575
+ " - Do not ask questions already answered above."
576
+ )
577
+
578
+ result = await _run_structured_agent(
579
+ system_prompt=(
580
+ "## ROLE\n"
581
+ "You are a CAD planning assistant applying the Printer-Profile-Tolerancing skill.\n"
582
+ "Normalize goals into manufacturing-ready language with explicit tolerance/clearance "
583
+ "targets (e.g. '0.30 mm mating clearance', '0.2 mm layer height'). "
584
+ "Ask only the highest-leverage questions that unblock the SolidWorks modeling steps. "
585
+ "Always surface material, nozzle size, and orientation constraints when present in the goal. "
586
+ "If the user has already supplied explicit dimensions, wall thickness, fit targets, and feature placement, do not keep restating the same asks; return zero questions instead."
587
+ ),
588
+ user_prompt=prompt,
589
+ result_type=ClarificationResponse,
590
+ model_name=resolved_model,
591
+ local_endpoint=resolved_endpoint,
592
+ )
593
+
594
+ if isinstance(result, RecoverableFailure):
595
+ merge_metadata(
596
+ session_id,
597
+ db_path=db_path,
598
+ latest_message=result.explanation,
599
+ clarifying_questions=[],
600
+ latest_error_text=result.explanation,
601
+ remediation_hint=(
602
+ result.remediation_steps[0]
603
+ if result.remediation_steps
604
+ else "Configure provider credentials and retry."
605
+ ),
606
+ )
607
+ insert_tool_call_record(
608
+ session_id=session_id,
609
+ tool_name="ui.request_clarifications",
610
+ input_json=json.dumps({"user_goal": user_goal}, ensure_ascii=True),
611
+ output_json=result.model_dump_json(),
612
+ success=False,
613
+ db_path=db_path,
614
+ )
615
+ return build_dashboard_state(session_id, db_path=db_path)
616
+
617
+ metadata = merge_metadata(
618
+ session_id,
619
+ db_path=db_path,
620
+ user_goal=user_goal,
621
+ normalized_brief=result.normalized_brief,
622
+ clarifying_questions=result.questions,
623
+ user_clarification_answer=user_answer,
624
+ latest_message="Generated clarifying questions from GitHub Copilot.",
625
+ latest_error_text="",
626
+ remediation_hint="",
627
+ )
628
+ insert_tool_call_record(
629
+ session_id=session_id,
630
+ tool_name="ui.request_clarifications",
631
+ input_json=json.dumps({"user_goal": user_goal}, ensure_ascii=True),
632
+ output_json=result.model_dump_json(),
633
+ success=True,
634
+ db_path=db_path,
635
+ )
636
+ insert_evidence_link(
637
+ session_id=session_id,
638
+ source_type="llm",
639
+ source_id="clarification_response",
640
+ relevance_score=0.9,
641
+ rationale="Normalized brief and follow-up questions from GitHub Copilot.",
642
+ payload_json=json.dumps(metadata, ensure_ascii=True),
643
+ db_path=db_path,
644
+ )
645
+ return build_dashboard_state(session_id, db_path=db_path)
646
+
647
+
648
+ async def inspect_family(
649
+ session_id: str,
650
+ user_goal: str,
651
+ *,
652
+ db_path: Path | None = None,
653
+ model_name: str | None = None,
654
+ ) -> dict[str, Any]:
655
+ """Run LLM-backed family classification and suggested checkpoints.
656
+
657
+ Args:
658
+ session_id: Dashboard session identifier.
659
+ user_goal: Free-text description of the design intent.
660
+ db_path: Optional SQLite path override.
661
+ model_name: Optional provider-qualified model override.
662
+
663
+ Returns:
664
+ Full dashboard state payload.
665
+ """
666
+ from .session_service import ( # noqa: PLC0415
667
+ build_dashboard_state,
668
+ ensure_dashboard_session,
669
+ )
670
+
671
+ ensure_dashboard_session(session_id, user_goal=user_goal, db_path=db_path)
672
+ session_row = get_design_session(session_id, db_path=db_path) or {}
673
+ meta = _parse_json_blob(session_row.get("metadata_json"))
674
+ resolved_model = normalize_model_name_for_provider(
675
+ model_name or meta.get("model_name"),
676
+ provider=sanitize_ui_text(meta.get("model_provider"), "github"),
677
+ profile=sanitize_ui_text(meta.get("model_profile"), "balanced"),
678
+ )
679
+ resolved_endpoint = sanitize_ui_text(
680
+ meta.get("local_endpoint"),
681
+ os.getenv("SOLIDWORKS_UI_LOCAL_ENDPOINT", "http://127.0.0.1:11434/v1"),
682
+ )
683
+
684
+ local_family = meta.get("proposed_family", "") or "<not yet classified>"
685
+ local_evidence = " | ".join(meta.get("family_evidence", [])) or "<none>"
686
+ prompt = (
687
+ "## TASK\n"
688
+ "Apply the Feature-Tree-Reconstruction skill to classify the SolidWorks feature family "
689
+ "and produce a human-reviewable checkpoint plan.\n\n"
690
+ "## DESIGN GOAL\n"
691
+ f"{user_goal}\n\n"
692
+ "## MODEL CONTEXT\n"
693
+ f"Active model path : {meta.get('active_model_path', '') or '<none>'}\n"
694
+ f"Active model status: {meta.get('active_model_status', '') or '<none>'}\n"
695
+ f"Feature target refs: {meta.get('feature_target_text', '') or '<none>'}\n"
696
+ f"Feature target status: {meta.get('feature_target_status', '') or '<none>'}\n\n"
697
+ "## LOCAL CLASSIFIER EVIDENCE (pre-computed)\n"
698
+ f"Family : {local_family}\n"
699
+ f"Evidence: {local_evidence}\n\n"
700
+ "## REFERENCE CORPUS\n"
701
+ f"{meta.get('rag_provenance_text', '') or '<none>'}\n\n"
702
+ "## FEATURE-TREE RECONSTRUCTION SKILL\n"
703
+ "Inspection sequence when model is available (use your mcp tools):\n"
704
+ " open_model → get_model_info → list_features(include_suppressed=True) "
705
+ "→ get_mass_properties → classify_feature_tree\n"
706
+ "Feature families: revolve | extrude | sheet_metal | advanced_solid | assembly | drawing | unknown\n"
707
+ "Delegation rules:\n"
708
+ " - sheet_metal or advanced_solid → VBA-aware reconstruction path\n"
709
+ " - simple part family → direct MCP checkpoint plan\n"
710
+ " - assembly → component-first decomposition, part-level plan per component\n"
711
+ "Guardrail: never reconstruct from silhouette only. "
712
+ "If confidence is low and contradictory evidence exists, propose more inspection steps. "
713
+ "When no model is attached, treat explicit user-supplied dimensions, named sketch phases, feature ordering, and manufacturing constraints as valid user-confirmed evidence for a conservative plan.\n\n"
714
+ "## OUTPUT CONTRACT\n"
715
+ "Return: family, confidence (high/medium/low), evidence[], warnings[], checkpoints[3-6].\n"
716
+ "You must always emit at least 3 checkpoints when the prompt contains enough explicit geometry to start from an empty part, even if confidence is only low or medium.\n"
717
+ "Each checkpoint: title, allowed_tools[] (from MCP tool catalog), rationale."
718
+ )
719
+
720
+ result = await _run_structured_agent(
721
+ system_prompt=(
722
+ "## ROLE\n"
723
+ "You are a SolidWorks routing assistant applying the Feature-Tree-Reconstruction skill.\n"
724
+ "Classify the feature family with evidence and confidence, then produce a safe "
725
+ "checkpoint plan for human review.\n\n"
726
+ "## ORCHESTRATION NOTES\n"
727
+ " - Inspection before planning: never produce a build plan without at least one "
728
+ "evidence item from model inspection or user-confirmed context.\n"
729
+ " - Explicit user-specified dimensions, feature names, and operation ordering count as user-confirmed context when no model is attached.\n"
730
+ " - Propose 3-6 conservative checkpoints. Require human confirmation before each "
731
+ "irreversible step.\n"
732
+ " - For sheet metal or unsupported advanced features, route to VBA-aware planning.\n"
733
+ " - Surface warnings when evidence is contradictory or confidence is low.\n"
734
+ " - Prefer 'extrude' for prompt-only parts built from named sketches and base extrusions unless stronger contrary evidence exists."
735
+ ),
736
+ user_prompt=prompt,
737
+ result_type=FamilyInspection,
738
+ model_name=resolved_model,
739
+ local_endpoint=resolved_endpoint,
740
+ )
741
+
742
+ if isinstance(result, RecoverableFailure):
743
+ ordered_features = _extract_explicit_feature_order(user_goal)
744
+ if ordered_features:
745
+ result = _coerce_explicit_feature_order_plan(
746
+ user_goal,
747
+ FamilyInspection(
748
+ family="extrude",
749
+ confidence="medium",
750
+ evidence=[
751
+ "Deterministic fallback engaged because model routing failed.",
752
+ "Goal includes an explicit feature-order sequence.",
753
+ ],
754
+ warnings=[
755
+ result.explanation,
756
+ "Using explicit feature-order checkpoints until model routing is healthy.",
757
+ ],
758
+ checkpoints=[],
759
+ ),
760
+ )
761
+ else:
762
+ merge_metadata(
763
+ session_id,
764
+ db_path=db_path,
765
+ latest_message=result.explanation,
766
+ latest_error_text=result.explanation,
767
+ remediation_hint=(
768
+ result.remediation_steps[0]
769
+ if result.remediation_steps
770
+ else "Adjust provider/model settings, then retry inspect."
771
+ ),
772
+ )
773
+ insert_tool_call_record(
774
+ session_id=session_id,
775
+ tool_name="ui.inspect_family",
776
+ input_json=json.dumps({"user_goal": user_goal}, ensure_ascii=True),
777
+ output_json=result.model_dump_json(),
778
+ success=False,
779
+ db_path=db_path,
780
+ )
781
+ return build_dashboard_state(session_id, db_path=db_path)
782
+
783
+ result = _coerce_explicit_feature_order_plan(user_goal, result)
784
+
785
+ evidence_payload = []
786
+ for index, line in enumerate(result.evidence, start=1):
787
+ insert_evidence_link(
788
+ session_id=session_id,
789
+ source_type="llm",
790
+ source_id=f"family_evidence_{index}",
791
+ relevance_score=0.85,
792
+ rationale=line,
793
+ payload_json=json.dumps({"family": result.family}, ensure_ascii=True),
794
+ db_path=db_path,
795
+ )
796
+ evidence_payload.append(line)
797
+
798
+ if result.checkpoints:
799
+ replacement_rows: list[dict[str, Any]] = []
800
+ for index, checkpoint in enumerate(result.checkpoints, start=1):
801
+ replacement_rows.append(
802
+ {
803
+ "checkpoint_index": index,
804
+ "title": checkpoint.title,
805
+ "planned_action_json": json.dumps(
806
+ {
807
+ "title": checkpoint.title,
808
+ "goal": checkpoint.title,
809
+ "tools": checkpoint.allowed_tools,
810
+ "rationale": checkpoint.rationale,
811
+ **checkpoint.execution,
812
+ },
813
+ ensure_ascii=True,
814
+ ),
815
+ "approved_by_user": index == 1,
816
+ }
817
+ )
818
+
819
+ replace_plan_checkpoints(
820
+ session_id=session_id,
821
+ checkpoints=replacement_rows,
822
+ db_path=db_path,
823
+ )
824
+
825
+ metadata = merge_metadata(
826
+ session_id,
827
+ db_path=db_path,
828
+ user_goal=user_goal,
829
+ proposed_family=result.family,
830
+ family_confidence=result.confidence,
831
+ family_evidence=evidence_payload,
832
+ family_warnings=result.warnings,
833
+ latest_message=f"Updated family classification to '{result.family}' from GitHub Copilot.",
834
+ latest_error_text="",
835
+ remediation_hint="",
836
+ )
837
+ insert_tool_call_record(
838
+ session_id=session_id,
839
+ tool_name="ui.inspect_family",
840
+ input_json=json.dumps({"user_goal": user_goal}, ensure_ascii=True),
841
+ output_json=result.model_dump_json(),
842
+ success=True,
843
+ db_path=db_path,
844
+ )
845
+ insert_evidence_link(
846
+ session_id=session_id,
847
+ source_type="llm",
848
+ source_id="family_inspection",
849
+ relevance_score=0.93,
850
+ rationale="LLM family classification and checkpoint suggestions from GitHub Copilot.",
851
+ payload_json=json.dumps(metadata, ensure_ascii=True),
852
+ db_path=db_path,
853
+ )
854
+ return build_dashboard_state(session_id, db_path=db_path)
855
+
856
+
857
+ async def run_go_orchestration(
858
+ session_id: str,
859
+ *,
860
+ user_goal: str,
861
+ assumptions_text: str | None = None,
862
+ user_answer: str = "",
863
+ db_path: Path | None = None,
864
+ api_origin: str = DEFAULT_API_ORIGIN,
865
+ ) -> dict[str, Any]:
866
+ """Run a single end-to-end pass: approve brief, update preferences, clarify, inspect.
867
+
868
+ Args:
869
+ session_id: Dashboard session identifier.
870
+ user_goal: Free-text description of the design intent.
871
+ assumptions_text: Optional manufacturing assumptions to persist.
872
+ user_answer: Any previous answers the user has already provided.
873
+ db_path: Optional SQLite path override.
874
+ api_origin: Base URL of the running FastAPI server.
875
+
876
+ Returns:
877
+ Full dashboard state payload.
878
+ """
879
+ from .session_service import ( # noqa: PLC0415
880
+ approve_design_brief,
881
+ build_dashboard_state,
882
+ update_ui_preferences,
883
+ )
884
+
885
+ try:
886
+ goal_text = sanitize_ui_text(user_goal, DEFAULT_USER_GOAL)
887
+ approve_design_brief(session_id, goal_text, db_path=db_path)
888
+
889
+ session_row = get_design_session(session_id, db_path=db_path) or {}
890
+ meta = _parse_json_blob(session_row.get("metadata_json"))
891
+ update_ui_preferences(
892
+ session_id,
893
+ assumptions_text=assumptions_text,
894
+ model_provider=str(meta.get("model_provider") or "github"),
895
+ model_profile=str(meta.get("model_profile") or "balanced"),
896
+ model_name=meta.get("model_name"),
897
+ local_endpoint=meta.get("local_endpoint"),
898
+ db_path=db_path,
899
+ )
900
+
901
+ await request_clarifications(
902
+ session_id,
903
+ goal_text,
904
+ user_answer=user_answer,
905
+ db_path=db_path,
906
+ )
907
+ await inspect_family(session_id, goal_text, db_path=db_path)
908
+
909
+ persist_ui_action(
910
+ session_id,
911
+ tool_name="ui.orchestrate_go",
912
+ db_path=db_path,
913
+ metadata_updates={
914
+ "orchestration_status": (
915
+ "Go run completed: inputs saved, clarifications refreshed, engineering review updated."
916
+ ),
917
+ "latest_message": "Go run completed across workflow, review, and model output lanes.",
918
+ "latest_error_text": "",
919
+ "remediation_hint": "",
920
+ },
921
+ input_payload={
922
+ "user_goal": goal_text,
923
+ "assumptions_text": assumptions_text,
924
+ "user_answer": user_answer,
925
+ },
926
+ output_payload={
927
+ "status": "success",
928
+ "message": "Go orchestration completed.",
929
+ },
930
+ )
931
+ except Exception as exc:
932
+ logger.exception("[ui.run_go_orchestration] failed session_id={}", session_id)
933
+ merge_metadata(
934
+ session_id,
935
+ db_path=db_path,
936
+ orchestration_status="Go run failed.",
937
+ latest_error_text=str(exc),
938
+ remediation_hint="Review provider credentials/model selection and retry Go.",
939
+ )
940
+ return build_dashboard_state(session_id, db_path=db_path, api_origin=api_origin)
941
+
942
+
943
+ # ---------------------------------------------------------------------------
944
+ # Private JSON helper (local to this module)
945
+ # ---------------------------------------------------------------------------
946
+
947
+
948
+ def _parse_json_blob(payload: str | None) -> dict[str, Any]:
949
+ """Parse a JSON string into a dict, returning an empty dict on failure."""
950
+ if not payload:
951
+ return {}
952
+ try:
953
+ result = json.loads(payload)
954
+ return result if isinstance(result, dict) else {}
955
+ except (json.JSONDecodeError, TypeError, ValueError):
956
+ return {}