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
Under the hood, analyze() runs five stages — load_step → build_topology → detect_features → compute_spatial → render_tier0 — each returning plain Python dataclasses, with no OCC objects leaking past the ingest boundary. Every stage is importable on its own (see the API reference below).
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.