Build AI tools that
understand CAD files
cadvert is a Python library that reads STEP, IGES, BREP, STL, and OBJ files
and converts them into structured text context your LLM can reason about —
geometry, features, tolerances, dimensions, and rendered views, all in one call.
pip install cadvert
Installation
The base package includes CAD parsing and full geometry analysis. Install optional extras for additional capabilities:
# Core — CAD parsing, topology, features, spatial analysis
pip install cadvert
# + REST API server (FastAPI + Uvicorn)
pip install cadvert[server]
# + LLM integration (OpenAI)
pip install cadvert[llm]
# + Mesh support (OBJ files via trimesh)
pip install cadvert[mesh]
# Everything
pip install cadvert[full]
Requires Python 3.10+. The core package ships cadquery-ocp (OCCT bindings) and numpy — no other heavy dependencies.
Quick start
One call takes a CAD file to LLM-ready context — and every other representation:
import cadvert
result = cadvert.analyze("bracket.step")
print(result.to_text()) # LLM-ready structured document (the flagship)
result.to_dict() # JSON-safe analysis for quoting / CI / databases
result.to_json()
result.to_graph() # networkx face-adjacency graph (GNN / UV-Net)
result.to_points(2048) # (N,3) surface point cloud (PointNet / 3D-CNN)
analyze() runs the full pipeline (ingest → topology → features → spatial) and returns a CadvertResult that exposes every representation from one object. to_text() is a compact document (~5–15 KB) with the part's global properties, geometry, detected features, and spatial relationships — everything an LLM needs to reason about the file.
cadvert.ingest,
cadvert.topology, cadvert.features, cadvert.spatial,
cadvert.document) — see the API reference below. analyze()
just wires them together for the common case.
analyze() also handles .dxf 2D drawings, returning a DxfResult with the same to_text() / to_dict() interface — entities, layers, dimensions, and title-block text.
How it works
cadvert runs a five-stage pipeline under the hood. Each stage returns plain Python dataclasses — no OCC objects leak out.
What's in the output document?
| Section | What's included |
|---|---|
| Global properties | Bounding box, volume, surface area, center of mass, body count |
| Topology map | All faces and edges with surface types (plane, cylinder, sphere, NURBS…) |
| Detected features | Holes (diameter/depth), bosses, fillets, chamfers, slots, patterns |
| Spatial relationships | Wall thicknesses, feature distances, draft angles, undercuts |
| GD&T annotations | Tolerances from AP242 STEP files with PMI (if present) |
Supported formats
| Extension | Format | Pipeline | Features & Spatial |
|---|---|---|---|
.step / .stp |
STEP ISO 10303 (AP203, AP214, AP242) | Full B-REP | Yes — including GD&T for AP242 |
.iges / .igs |
IGES 5.x | Full B-REP | Yes |
.brep |
OpenCASCADE native | Full B-REP | Yes |
.stl |
STereoLithography | Mesh only | Global properties only |
.obj |
Wavefront OBJ | Mesh only | Global properties only (requires cadvert[mesh]) |
API reference
cadvert.ingest
Entry point for loading any supported CAD format.
from cadvert.ingest import load_step, IngestError, PartMetadata
shape, body_count, metadata = load_step(path: str | Path)
# shape → OCC TopoDS_Shape (pass to build_topology / render_shape)
# body_count → int, number of distinct solid bodies (0 for mesh files)
# metadata → PartMetadata dataclass
# PartMetadata fields:
metadata.source_format # "STEP" | "IGES" | "BREP" | "STL" | "OBJ"
metadata.is_mesh # True for STL/OBJ — triggers mesh-only pipeline
metadata.units # "mm" | "inch" | "m" | ...
metadata.unit_scale_to_mm # float multiplier to convert to mm
metadata.schema # "AP203" | "AP214" | "AP242" (STEP only)
metadata.originating_system # CAD software that authored the file
metadata.description # FILE_DESCRIPTION from STEP header
metadata.triangle_count # populated for mesh formats
metadata.gdt_annotations # list[GDTAnnotation] (AP242 PMI only)
# Raises IngestError on unsupported format or parse failure
cadvert.topology
Extracts the full face/edge/vertex graph from a loaded B-REP shape.
from cadvert.topology import build_topology, TopologyGraph
graph = build_topology(shape, body_count) # → TopologyGraph
graph.faces # list[FaceInfo] — surfaces with geometry + connectivity
graph.edges # list[EdgeInfo] — curves with dihedral angles + convexity
graph.vertices # list[VertexInfo]
graph.bounding_box # {"X": (min, max), "Y": (...), "Z": (...)}
graph.volume # float, native units
graph.surface_area # float, native units
graph.center_of_mass # (x, y, z)
graph.body_count # int
# FaceInfo.geometry dict — surface type determines fields:
# PLANE: {"type": "PLANE", "normal": (nx,ny,nz), "origin": (x,y,z), "d": offset}
# CYLINDER: {"type": "CYLINDER", "axis_origin": ..., "axis_dir": ..., "radius": r}
# CONE: {"type": "CONE", "apex": ..., "axis_dir": ..., "half_angle": deg}
# SPHERE: {"type": "SPHERE", "center": ..., "radius": r}
# TORUS: {"type": "TORUS", "center": ..., "major_radius": R, "minor_radius": r}
# NURBS_SURFACE: {"type": "NURBS_SURFACE", "degree_u": ..., "degree_v": ..., ...}
# EdgeInfo.convexity: "convex" | "concave" | "tangent"
cadvert.features
Detects manufacturing features from the topology graph.
from cadvert.features import detect_features, DetectedFeature
features = detect_features(graph) # → list[DetectedFeature]
# DetectedFeature fields:
feature.feature_type # "THROUGH_HOLE" | "BLIND_HOLE" | "BOSS" | "FILLET" |
# "CHAMFER" | "POCKET" | "COUNTERBORE" | "COUNTERSINK" |
# "SLOT" | "PATTERN"
feature.face_ids # list[int] — which faces comprise this feature
feature.edge_ids # list[int]
feature.parameters # dict — feature-specific (e.g. diameter, depth, axis for holes)
feature.confidence # float 0.0–1.0 (features below 0.4 are suppressed)
feature.standard_match # str | None — e.g. "M6 clearance", "1/4\" thru"
feature.notes # human-readable description
cadvert.spatial
Computes distances, wall thicknesses, draft angles, and undercut detection.
from cadvert.spatial import compute_spatial_relationships
spatial = compute_spatial_relationships(
graph,
features,
shape=shape, # optional — enables advanced ray-based analysis
pull_direction=(0, 0, 1), # for draft angle / undercut analysis
) # → list[SpatialRelationship]
# SpatialRelationship fields:
rel.description # "Wall thickness" | "Draft angle" | "Distance" | ...
rel.value # float — mm (or degrees for angles)
rel.from_ref # e.g. "hole_1", "F42"
rel.to_ref # e.g. "outer_surface", "F10"
rel.notes # additional context
cadvert.document
Renders the analysis results into structured text for LLM consumption.
from cadvert.document import render_tier0, render_document
# Compact summary — best for LLM system prompts (~5–15 KB)
text = render_tier0(
graph,
source_path,
features=features, # optional
spatial=spatial, # optional
units=metadata.units,
gdt_annotations=metadata.gdt_annotations, # optional
)
# Full multi-section HSD document — complete topology dump
full_text = render_document(
graph,
source_path,
part_name=None, # override display name
features=features,
spatial=spatial,
rendered_views=views, # list of PNG paths from render_shape()
validation_report=None,
units=metadata.units,
gdt_annotations=metadata.gdt_annotations,
mesh_info=None,
)
cadvert.renderer
Renders 8 standard orthographic + isometric views as PNG files.
from cadvert.renderer import render_shape, render_views
# From a pre-loaded OCC shape
views = render_shape(
shape,
output_dir="./views",
image_size=(1200, 900),
stem="part",
) # → list[Path] — front, back, left, right, top, bottom, iso, ...
# Convenience: load + render in one call
views = render_views(
step_path="bracket.step",
output_dir="./views",
image_size=(1200, 900),
)
Building an LLM tool
The recommended pattern is to wrap cadvert's pipeline into a single function that you register as a tool in your LLM framework. The tool receives a file path, runs the full pipeline, and returns the structured text context.
from cadvert.ingest import load_step, IngestError
from cadvert.topology import build_topology
from cadvert.features import detect_features
from cadvert.spatial import compute_spatial_relationships
from cadvert.document import render_tier0
from cadvert.renderer import render_shape
def analyze_cad(file_path: str) -> dict:
"""
Load a CAD file and return structured context for an LLM.
Supports .step, .stp, .iges, .igs, .brep, .stl, .obj
"""
try:
shape, body_count, metadata = load_step(file_path)
except IngestError as e:
return {"error": str(e)}
result = {
"format": metadata.source_format,
"units": metadata.units,
"is_mesh": metadata.is_mesh,
}
if not metadata.is_mesh:
graph = build_topology(shape, body_count)
features = detect_features(graph)
spatial = compute_spatial_relationships(graph, features, shape=shape)
result["context"] = render_tier0(
graph, file_path,
features=features,
spatial=spatial,
units=metadata.units,
gdt_annotations=metadata.gdt_annotations,
)
else:
# Mesh files: global properties only
result["context"] = f"Mesh file: {metadata.triangle_count} triangles."
# Optional: render views and return image paths
# result["views"] = [str(p) for p in render_shape(shape, "./views")]
return result
Example: Claude tool use
Register analyze_cad as a tool in an Anthropic API call.
Claude will invoke it when the user references a CAD file,
then reason over the structured output.
export ANTHROPIC_API_KEY="sk-ant-..."The
anthropic.Anthropic() client reads it automatically. You can also pass it directly: anthropic.Anthropic(api_key="sk-ant-...")
import anthropic
import json
client = anthropic.Anthropic()
tools = [
{
"name": "analyze_cad",
"description": (
"Read a CAD file (.step, .iges, .brep, .stl, .obj) and return "
"structured geometry, detected features (holes, bosses, fillets, …), "
"spatial relationships, dimensions, and tolerances."
),
"input_schema": {
"type": "object",
"properties": {
"file_path": {
"type": "string",
"description": "Absolute path to the CAD file on disk"
}
},
"required": ["file_path"]
}
}
]
def run_agent(user_message: str):
messages = [{"role": "user", "content": user_message}]
while True:
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=4096,
tools=tools,
messages=messages,
)
# Append assistant turn
messages.append({"role": "assistant", "content": response.content})
if response.stop_reason == "tool_use":
tool_results = []
for block in response.content:
if block.type == "tool_use" and block.name == "analyze_cad":
result = analyze_cad(block.input["file_path"])
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": json.dumps(result),
})
messages.append({"role": "user", "content": tool_results})
else:
# Final text response
for block in response.content:
if hasattr(block, "text"):
print(block.text)
break
run_agent("Analyze bracket.step and tell me if it's suitable for CNC machining.")
render_shape() alongside the text context
to a vision-capable model (Claude, GPT-4o) for even richer analysis.
The text covers numbers; the images cover spatial intuition.
CLI reference
cadvert also ships a command-line tool for one-off conversions:
# Convert any supported file to an HSD document
cadvert part.step -o output.hsd.txt
# Skip specific pipeline stages
cadvert part.step --no-features --no-spatial --no-render --no-validate
# Include full NURBS control-point data
cadvert part.step --nurbs-detail
# Set draft analysis direction
cadvert part.step --pull-direction 0,0,-1
# Override render resolution
cadvert part.step --image-size 1600x1200
# Start the REST API server (requires cadvert[server])
cadvert-server
# Listens on http://localhost:8000
part.hsd.txt (summary) and part.full_topology.txt
(complete face/edge dump). Pass the summary to your LLM system prompt;
fetch the full topology on demand.