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,164 @@
1
+ """Local retrieval index builder for feature-tree audits, tool docs, and failures."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from datetime import UTC, datetime
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from .history_db import DEFAULT_DB_PATH, find_recent_errors
11
+
12
+
13
+ def _chunk_text(text: str, chunk_size: int = 1000, overlap: int = 150) -> list[str]:
14
+ """Split long text into overlapping chunks for simple local retrieval.
15
+
16
+ Args:
17
+ text (str): Input text processed by the operation.
18
+ chunk_size (int): Maximum number of characters to keep in each chunk. Defaults to
19
+ 1000.
20
+ overlap (int): Number of overlapping characters between chunks. Defaults to 150.
21
+
22
+ Returns:
23
+ list[str]: A list containing the resulting items.
24
+ """
25
+ normalized = (text or "").strip()
26
+ if not normalized:
27
+ return []
28
+
29
+ if len(normalized) <= chunk_size:
30
+ return [normalized]
31
+
32
+ chunks: list[str] = []
33
+ start = 0
34
+ while start < len(normalized):
35
+ end = min(start + chunk_size, len(normalized))
36
+ chunks.append(normalized[start:end])
37
+ if end == len(normalized):
38
+ break
39
+ start = max(0, end - overlap)
40
+ return chunks
41
+
42
+
43
+ def _read_text(path: Path) -> str:
44
+ """Build internal text.
45
+
46
+ Args:
47
+ path (Path): Filesystem path for the operation.
48
+
49
+ Returns:
50
+ str: The resulting text value.
51
+ """
52
+
53
+ try:
54
+ return path.read_text(encoding="utf-8")
55
+ except OSError:
56
+ return ""
57
+
58
+
59
+ def build_local_retrieval_index(
60
+ *,
61
+ output_path: Path | None = None,
62
+ worked_examples_path: Path | None = None,
63
+ tool_catalog_dir: Path | None = None,
64
+ db_path: Path | None = None,
65
+ max_recent_errors: int = 200,
66
+ ) -> dict[str, Any]:
67
+ """Build a JSON retrieval index from local audits, tool docs, and failures.
68
+
69
+ Args:
70
+ output_path (Path | None): The output path value. Defaults to None.
71
+ worked_examples_path (Path | None): The worked examples path value. Defaults to
72
+ None.
73
+ tool_catalog_dir (Path | None): The tool catalog dir value. Defaults to None.
74
+ db_path (Path | None): The db path value. Defaults to None.
75
+ max_recent_errors (int): The max recent errors value. Defaults to 200.
76
+
77
+ Returns:
78
+ dict[str, Any]: A dictionary containing the resulting values.
79
+ """
80
+ if output_path is None:
81
+ output_path = Path(".solidworks_mcp") / "retrieval" / "local_index.json"
82
+ if worked_examples_path is None:
83
+ worked_examples_path = Path("docs") / "user-guide" / "worked-examples.md"
84
+ if tool_catalog_dir is None:
85
+ tool_catalog_dir = Path("docs") / "user-guide" / "tool-catalog"
86
+ if db_path is None:
87
+ db_path = DEFAULT_DB_PATH
88
+
89
+ chunks: list[dict[str, Any]] = []
90
+ chunk_id = 0
91
+
92
+ worked_text = _read_text(worked_examples_path)
93
+ for idx, chunk in enumerate(_chunk_text(worked_text), start=1):
94
+ chunk_id += 1
95
+ chunks.append(
96
+ {
97
+ "id": f"audit-worked-examples-{idx}",
98
+ "chunk_id": chunk_id,
99
+ "source_type": "feature_tree_audit",
100
+ "source": str(worked_examples_path),
101
+ "text": chunk,
102
+ "tags": ["audit", "worked_examples", "inspect_classify_delegate"],
103
+ }
104
+ )
105
+
106
+ if tool_catalog_dir.exists():
107
+ for doc_path in sorted(tool_catalog_dir.glob("*.md")):
108
+ if doc_path.name.lower() == "index.md":
109
+ continue
110
+ doc_text = _read_text(doc_path)
111
+ for idx, chunk in enumerate(_chunk_text(doc_text), start=1):
112
+ chunk_id += 1
113
+ chunks.append(
114
+ {
115
+ "id": f"tool-doc-{doc_path.stem}-{idx}",
116
+ "chunk_id": chunk_id,
117
+ "source_type": "tool_doc",
118
+ "source": str(doc_path),
119
+ "text": chunk,
120
+ "tags": ["tool_catalog", doc_path.stem],
121
+ }
122
+ )
123
+
124
+ for idx, error in enumerate(
125
+ find_recent_errors(limit=max_recent_errors, db_path=db_path),
126
+ start=1,
127
+ ):
128
+ chunk_id += 1
129
+ error_text = (
130
+ f"source={error.get('source')} | tool={error.get('tool_name')} | "
131
+ f"type={error.get('error_type')} | message={error.get('error_message')} | "
132
+ f"root_cause={error.get('root_cause')} | remediation={error.get('remediation')}"
133
+ )
134
+ chunks.append(
135
+ {
136
+ "id": f"failure-memory-{idx}",
137
+ "chunk_id": chunk_id,
138
+ "source_type": "failure_memory",
139
+ "source": str(db_path),
140
+ "text": error_text,
141
+ "tags": ["failure_memory", error.get("tool_name", "unknown")],
142
+ "created_at": error.get("created_at"),
143
+ }
144
+ )
145
+
146
+ output_path.parent.mkdir(parents=True, exist_ok=True)
147
+
148
+ payload: dict[str, Any] = {
149
+ "version": "1.0",
150
+ "generated_at": datetime.now(UTC).isoformat(),
151
+ "stats": {
152
+ "chunk_count": len(chunks),
153
+ "worked_examples_source": str(worked_examples_path),
154
+ "tool_catalog_source": str(tool_catalog_dir),
155
+ "failure_memory_source": str(db_path),
156
+ },
157
+ "chunks": chunks,
158
+ }
159
+
160
+ output_path.write_text(
161
+ json.dumps(payload, indent=2, ensure_ascii=True),
162
+ encoding="utf-8",
163
+ )
164
+ return payload
@@ -0,0 +1,165 @@
1
+ """Typed output schemas for agent prompt validation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Literal
6
+
7
+ from pydantic import BaseModel, Field
8
+
9
+
10
+ class Assumption(BaseModel):
11
+ """A single explicit assumption in an agent response.
12
+
13
+ Attributes:
14
+ statement (str): The statement value.
15
+ """
16
+
17
+ statement: str = Field(min_length=3)
18
+
19
+
20
+ class Recommendation(BaseModel):
21
+ """A prioritized recommendation with rationale and risk.
22
+
23
+ Attributes:
24
+ rationale (str): The rationale value.
25
+ risk (Literal["low", "medium", "high"]): The risk value.
26
+ title (str): The title value.
27
+ """
28
+
29
+ title: str = Field(min_length=3)
30
+ rationale: str = Field(min_length=5)
31
+ risk: Literal["low", "medium", "high"]
32
+
33
+
34
+ class ManufacturabilityReview(BaseModel):
35
+ """Validation shape for printability-focused agent responses.
36
+
37
+ Attributes:
38
+ assumptions (list[Assumption]): The assumptions value.
39
+ build_volume_check (str): The build volume check value.
40
+ orientation_guidance (str): The orientation guidance value.
41
+ recommendations (list[Recommendation]): The recommendations value.
42
+ summary (str): The summary value.
43
+ tolerance_clearance_notes (list[str]): The tolerance clearance notes value.
44
+ """
45
+
46
+ summary: str = Field(min_length=10)
47
+ assumptions: list[Assumption] = Field(default_factory=list)
48
+ recommendations: list[Recommendation] = Field(default_factory=list)
49
+ orientation_guidance: str = Field(min_length=5)
50
+ tolerance_clearance_notes: list[str] = Field(default_factory=list)
51
+ build_volume_check: str = Field(
52
+ description="Expected to state pass/fail and mitigation options when needed.",
53
+ min_length=5,
54
+ )
55
+
56
+
57
+ class ToolRoutingDecision(BaseModel):
58
+ """Validation shape for tool-selection/skills documentation prompts.
59
+
60
+ Attributes:
61
+ fallback_strategy (list[str]): The fallback strategy value.
62
+ intent (str): The intent value.
63
+ selected_tool_group (str): The selected tool group value.
64
+ why (str): The why value.
65
+ """
66
+
67
+ intent: str = Field(min_length=3)
68
+ selected_tool_group: str = Field(min_length=3)
69
+ why: str = Field(min_length=10)
70
+ fallback_strategy: list[str] = Field(default_factory=list)
71
+
72
+
73
+ class DocsPlan(BaseModel):
74
+ """Validation shape for docs-engineering responses.
75
+
76
+ Attributes:
77
+ audience (str): The audience value.
78
+ decisions (list[ToolRoutingDecision]): The decisions value.
79
+ demo_steps (list[str]): The demo steps value.
80
+ objective (str): The objective value.
81
+ sections (list[str]): The sections value.
82
+ """
83
+
84
+ audience: str = Field(min_length=3)
85
+ objective: str = Field(min_length=5)
86
+ sections: list[str] = Field(default_factory=list)
87
+ decisions: list[ToolRoutingDecision] = Field(default_factory=list)
88
+ demo_steps: list[str] = Field(default_factory=list)
89
+
90
+
91
+ class FeatureStep(BaseModel):
92
+ """One step in a part reconstruction plan.
93
+
94
+ Attributes:
95
+ description (str): The description value.
96
+ mcp_call (str): The mcp call value.
97
+ step_number (int): The step number value.
98
+ tool_name (str): The tool name value.
99
+ """
100
+
101
+ step_number: int = Field(ge=1)
102
+ tool_name: str = Field(
103
+ min_length=2, description="MCP tool or action, e.g. create_extrusion"
104
+ )
105
+ description: str = Field(min_length=5)
106
+ mcp_call: str = Field(
107
+ min_length=5,
108
+ description="Python-style call string, e.g. create_extrusion(sketch_name='Sketch1', depth=10.0)",
109
+ )
110
+
111
+
112
+ class ReconstructionPlan(BaseModel):
113
+ """Structured plan for recreating a SolidWorks part from scratch using MCP tools.
114
+
115
+ Attributes:
116
+ analysis_summary (str): The analysis summary value.
117
+ assembly_mates (list[str]): The assembly mates value.
118
+ complexity_tier (Literal[1, 2, 3, 4]): The complexity tier value.
119
+ feature_sequence (list[FeatureStep]): The feature sequence value.
120
+ part_name (str): The part name value.
121
+ validation_strategy (str): The validation strategy value.
122
+ vba_required (bool): The vba required value.
123
+ """
124
+
125
+ part_name: str = Field(min_length=2)
126
+ complexity_tier: Literal[1, 2, 3, 4] = Field(
127
+ description="1=Simple revolve/extrude, 2=Multi-sketch, 3=VBA/loft, 4=Assembly"
128
+ )
129
+ analysis_summary: str = Field(
130
+ min_length=10,
131
+ description="Brief summary of the part's key geometry and design intent.",
132
+ )
133
+ feature_sequence: list[FeatureStep] = Field(
134
+ min_length=1, description="Ordered list of MCP tool calls to recreate the part."
135
+ )
136
+ vba_required: bool = Field(
137
+ description="True if any feature requires generate_vba_part_modeling + execute_macro."
138
+ )
139
+ assembly_mates: list[str] = Field(
140
+ default_factory=list,
141
+ description="Mate descriptions for assembly parts, e.g. 'coincident: shaft_axis to yoke_bore'.",
142
+ )
143
+ validation_strategy: str = Field(
144
+ min_length=5,
145
+ description="How to confirm the reconstruction matches the original (e.g. pixel diff, mass properties).",
146
+ )
147
+
148
+
149
+ class RecoverableFailure(BaseModel):
150
+ """Typed failure output used when agent needs user-guided retry.
151
+
152
+ Attributes:
153
+ explanation (str): The explanation value.
154
+ remediation_steps (list[str]): The remediation steps value.
155
+ retry_focus (str | None): The retry focus value.
156
+ should_retry (bool): The should retry value.
157
+ """
158
+
159
+ explanation: str = Field(min_length=8)
160
+ remediation_steps: list[str] = Field(default_factory=list)
161
+ retry_focus: str | None = Field(
162
+ default=None,
163
+ description="Optional hint on what to change in the next prompt.",
164
+ )
165
+ should_retry: bool = True
@@ -0,0 +1,285 @@
1
+ """CLI for running validated prompt smoke tests against custom agent files."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import os
7
+ from enum import StrEnum
8
+ from typing import Annotated
9
+
10
+ import typer
11
+
12
+ from .harness import pretty_json, run_validated_prompt
13
+ from .schemas import (
14
+ DocsPlan,
15
+ ManufacturabilityReview,
16
+ ReconstructionPlan,
17
+ RecoverableFailure,
18
+ )
19
+
20
+ app = typer.Typer(
21
+ name="smoke-test",
22
+ help="Run validated custom-agent prompt tests against the SolidWorks MCP agents.",
23
+ no_args_is_help=True,
24
+ )
25
+
26
+
27
+ class SchemaChoice(StrEnum):
28
+ """Handle schema choice.
29
+
30
+ Attributes:
31
+ docs (Any): The docs value.
32
+ manufacturability (Any): The manufacturability value.
33
+ reconstruction (Any): The reconstruction value.
34
+ """
35
+
36
+ manufacturability = "manufacturability"
37
+ docs = "docs"
38
+ reconstruction = "reconstruction"
39
+
40
+
41
+ def _resolve_model(
42
+ anthropic: bool,
43
+ claude_model: str,
44
+ github_models: bool,
45
+ github_model: str,
46
+ model: str | None,
47
+ ) -> str:
48
+ """Resolve effective model id from CLI options.
49
+
50
+ Args:
51
+ anthropic (bool): The anthropic value.
52
+ claude_model (str): The claude model value.
53
+ github_models (bool): The github models value.
54
+ github_model (str): The github model value.
55
+ model (str | None): The model value.
56
+
57
+ Returns:
58
+ str: The resulting text value.
59
+
60
+ Raises:
61
+ BadParameter: --model is required unless --github-models or --anthropic is enabled.
62
+ Recommended: --github-models (requires GH_TOKEN with models:read
63
+ scope).
64
+ """
65
+ if github_models:
66
+ return f"github:{github_model}"
67
+
68
+ if anthropic:
69
+ return f"anthropic:{claude_model}"
70
+
71
+ if not model:
72
+ raise typer.BadParameter(
73
+ "--model is required unless --github-models or --anthropic is enabled. "
74
+ "Recommended: --github-models (requires GH_TOKEN with models:read scope).",
75
+ param_hint="'--model'",
76
+ )
77
+
78
+ return model
79
+
80
+
81
+ def _ensure_provider_credentials(model: str) -> None:
82
+ """Provide actionable credential guidance before model invocation.
83
+
84
+ Args:
85
+ model (str): The model value.
86
+
87
+ Returns:
88
+ None: None.
89
+
90
+ Raises:
91
+ BadParameter: OPENAI_API_KEY is not set. Copilot subscription cannot be reused as an
92
+ OpenAI API key for local pydantic-ai scripts. Use --github-models
93
+ instead.
94
+ """
95
+ if model.startswith("github:"):
96
+ github_token = os.getenv("GITHUB_API_KEY") or os.getenv("GH_TOKEN")
97
+ if not github_token:
98
+ # Fall back to `gh auth token` if the gh CLI is available and authenticated.
99
+ try:
100
+ import subprocess
101
+
102
+ result = subprocess.run(
103
+ ["gh", "auth", "token"], capture_output=True, text=True, timeout=5
104
+ )
105
+ if result.returncode == 0:
106
+ github_token = result.stdout.strip()
107
+ except Exception:
108
+ pass
109
+ if not github_token:
110
+ raise typer.BadParameter(
111
+ "GitHub Models requires GITHUB_API_KEY (or GH_TOKEN) with models:read scope.\n"
112
+ "Options:\n"
113
+ " 1. Run: gh auth login (GitHub CLI — free with Copilot subscription)\n"
114
+ " 2. Or set: $env:GH_TOKEN = 'github_pat_...'",
115
+ param_hint="'--github-models'",
116
+ )
117
+ # pydantic-ai GitHubProvider looks for GITHUB_API_KEY.
118
+ os.environ.setdefault("GITHUB_API_KEY", github_token)
119
+ return
120
+
121
+ if model.startswith("anthropic:") and not os.getenv("ANTHROPIC_API_KEY"):
122
+ raise typer.BadParameter(
123
+ "ANTHROPIC_API_KEY is not set.\n"
124
+ "Export your key before running smoke_test:\n"
125
+ " $env:ANTHROPIC_API_KEY = 'sk-ant-...'\n"
126
+ "Or use --github-models to run via GitHub Models with a GitHub PAT.",
127
+ param_hint="'--anthropic'",
128
+ )
129
+
130
+ if model.startswith("openai:") and not os.getenv("OPENAI_API_KEY"):
131
+ raise typer.BadParameter(
132
+ "OPENAI_API_KEY is not set. Copilot subscription cannot be reused as an "
133
+ "OpenAI API key for local pydantic-ai scripts. "
134
+ "Use --github-models instead.",
135
+ param_hint="'--model'",
136
+ )
137
+
138
+
139
+ async def _run(
140
+ agent_file: str,
141
+ model_name: str,
142
+ prompt: str,
143
+ schema: SchemaChoice,
144
+ max_retries_on_recoverable: int,
145
+ ) -> int:
146
+ """Build internal run.
147
+
148
+ Args:
149
+ agent_file (str): The agent file value.
150
+ model_name (str): Embedding model name to use.
151
+ prompt (str): The prompt value.
152
+ schema (SchemaChoice): The schema value.
153
+ max_retries_on_recoverable (int): The max retries on recoverable value.
154
+
155
+ Returns:
156
+ int: The computed numeric result.
157
+ """
158
+
159
+ schema_type: (
160
+ type[ManufacturabilityReview] | type[ReconstructionPlan] | type[DocsPlan]
161
+ )
162
+
163
+ if schema == SchemaChoice.manufacturability:
164
+ schema_type = ManufacturabilityReview
165
+ elif schema == SchemaChoice.reconstruction:
166
+ schema_type = ReconstructionPlan
167
+ else:
168
+ schema_type = DocsPlan
169
+ result = await run_validated_prompt(
170
+ agent_file_name=agent_file,
171
+ model_name=model_name,
172
+ user_prompt=prompt,
173
+ result_type=schema_type,
174
+ max_retries_on_recoverable=max_retries_on_recoverable,
175
+ )
176
+
177
+ if isinstance(result, RecoverableFailure):
178
+ typer.echo("RecoverableFailure returned after retry attempts:")
179
+ typer.echo(pretty_json(result))
180
+ return 0
181
+
182
+
183
+ @app.command()
184
+ def run(
185
+ agent_file: Annotated[
186
+ str,
187
+ typer.Option(
188
+ "--agent-file", help="Agent filename in .github/agents/", show_default=False
189
+ ),
190
+ ],
191
+ prompt: Annotated[
192
+ str,
193
+ typer.Option("--prompt", help="User prompt to run", show_default=False),
194
+ ],
195
+ github_models: Annotated[
196
+ bool,
197
+ typer.Option(
198
+ "--github-models",
199
+ help="Use GitHub Models provider (recommended). Requires GH_TOKEN or GITHUB_API_KEY with models:read scope.",
200
+ ),
201
+ ] = False,
202
+ github_model: Annotated[
203
+ str,
204
+ typer.Option(
205
+ "--github-model",
206
+ help="GitHub Models catalog id, e.g. openai/gpt-4.1 or mistral-ai/mistral-large.",
207
+ ),
208
+ ] = "openai/gpt-4.1",
209
+ anthropic: Annotated[
210
+ bool,
211
+ typer.Option(
212
+ "--anthropic",
213
+ help="Use Anthropic Claude (requires ANTHROPIC_API_KEY with active billing).",
214
+ ),
215
+ ] = False,
216
+ claude_model: Annotated[
217
+ str,
218
+ typer.Option(
219
+ "--claude-model",
220
+ help="Anthropic model id, e.g. claude-sonnet-4-6 or claude-opus-4-6.",
221
+ ),
222
+ ] = "claude-sonnet-4-6",
223
+ model: Annotated[
224
+ str | None,
225
+ typer.Option(
226
+ "--model",
227
+ help="Explicit PydanticAI model spec, e.g. openai:gpt-4.1. Overridden by --anthropic/--github-models.",
228
+ ),
229
+ ] = None,
230
+ schema: Annotated[
231
+ SchemaChoice,
232
+ typer.Option(
233
+ "--schema", help="Validation schema expected from the model output."
234
+ ),
235
+ ] = SchemaChoice.manufacturability,
236
+ max_retries_on_recoverable: Annotated[
237
+ int,
238
+ typer.Option(
239
+ "--max-retries-on-recoverable",
240
+ help="Automatic retry attempts when model returns RecoverableFailure.",
241
+ ),
242
+ ] = 1,
243
+ ) -> None:
244
+ """Run a validated custom-agent prompt test and print structured JSON output.
245
+
246
+ Args:
247
+ agent_file: Agent filename under `.github/agents/`.
248
+ prompt: User prompt to execute against the agent.
249
+ github_models: Use GitHub Models provider.
250
+ github_model: GitHub model id when `github_models` is enabled.
251
+ anthropic: Use Anthropic provider.
252
+ claude_model: Anthropic model id when `anthropic` is enabled.
253
+ model: Explicit provider-qualified model (for example `openai:gpt-4.1`).
254
+ schema: Expected structured output schema.
255
+ max_retries_on_recoverable: Retries for `RecoverableFailure` outputs.
256
+
257
+ Returns:
258
+ None: None.
259
+
260
+ Raises:
261
+ SystemExit: Always raised with the command exit code.
262
+ """
263
+ model_name = _resolve_model(
264
+ anthropic, claude_model, github_models, github_model, model
265
+ )
266
+ _ensure_provider_credentials(model_name)
267
+ raise SystemExit(
268
+ asyncio.run(
269
+ _run(agent_file, model_name, prompt, schema, max_retries_on_recoverable)
270
+ )
271
+ )
272
+
273
+
274
+ def main() -> None:
275
+ """Handle main.
276
+
277
+ Returns:
278
+ None: None.
279
+ """
280
+
281
+ app()
282
+
283
+
284
+ if __name__ == "__main__":
285
+ main()