Drawing Intelligence Platform for AEC professionals.
Upload a DXF or vector PDF—or a DWG when the optional ODA File Converter is installed—describe what you need in plain English, and get structured edits, compliance reports, quantity takeoffs, health assessments, RFIs, and drawing summaries, all without the AI ever touching your original file.
Links: Gist One-Pager · Docs
cad-dxf-agent is a multi-capability platform that handles DXF drawings through natural language. Describe what you need — an edit, a compliance check, a quantity takeoff, a health report — and the platform classifies your intent, selects the right processing pipeline, and delivers structured results.
| Capability | Description |
|---|---|
| Edit | Move, rotate, copy, scale, mirror, delete entities; add lines, polylines, circles, arcs, text, blocks |
| Compliance | ADA/IBC/custom rule validation with findings and remediation guidance |
| Health Report | Drawing quality metrics — layer hygiene, entity stats, potential issues |
| Quantity Takeoff | Automated extraction of counts, lengths, areas from drawing entities |
| Summary | Plain-English structured narrative of drawing contents |
| RFI Generation | Automated Request For Information based on detected ambiguities |
| Zone Detection | Closed-loop room/area detection with area calculation |
| Revision Comparison | Diff two DXF versions, review changes, apply approved edits |
| Georeferencing | Resolve caller, .prj, or supported DXF GEODATA CRS metadata and transform drawing points to/from WGS84 |
| Agent Mode | Iterative multi-turn tool-use loop for complex requests (max 10 turns) |
- Safe edits — The LLM planner returns structured operations only. It never touches raw DXF data.
- Two-axis intent classification — Every prompt is classified by what (edit, analyze, compare, query, generate) and why (compliance, coordination, documentation, estimation, quality, general).
- Protected layers — Configurable layers (TITLE, TITLEBLOCK, SEAL, REVISION) cannot be edited. Enforced at both validator and tool executor levels.
- Save-as workflow — Original files are always preserved.
- Mock mode — The full pipeline works without an API key for testing and development.
| Supported | Not Yet |
|---|---|
| DXF files (2D) | DWG native editing; DWG import/export requires the separately installed ODA File Converter |
| Model space + paper space layouts | 3D entities |
| LINE, LWPOLYLINE, TEXT, MTEXT, INSERT, CIRCLE, ARC | Dimension regeneration |
| 13 edit operations (move, edit_text, delete, add_block, rotate, copy, scale, mirror, add_line, add_polyline, add_circle, add_arc, add_text) | Xrefs |
| Compliance validation (ADA/IBC/custom) | Title block revision table |
| Health reports + quality metrics | |
| Automated quantity takeoff | |
| Plain-English drawing summaries | |
| RFI generation | |
| Zone/room detection with area calc | |
| Optional entity elevations and projected/geographic CRS metadata | Full 3D solids and surfaces |
load_dxf() resolves coordinate references in fail-closed precedence order: an explicit caller
override, a same-name .prj sidecar, then supported DXF GEODATA. A present but invalid CRS, or a
known $INSUNITS value that contradicts a projected CRS, raises CRSResolutionError instead of
silently producing incorrect coordinates.
from cad_dxf_agent.core.crs import drawing_to_wgs84
from cad_dxf_agent.core.dxf_reader import load_dxf
from cad_dxf_agent.models.cad_schema import Point2D
context = load_dxf("site.dxf", crs="EPSG:26916")
longitude_latitude = drawing_to_wgs84(Point2D(x=500000, y=3600000), context.crs)| Agent-mode iterative tool-use | | | Protected layers + AI revision notes | | | Revision comparison (CLI + web) | | | Web app (Firebase + Cloud Run, auto-deploy) | | | Desktop app (Windows + Linux) | | | Pluggable LLM provider (bring your own) | | | OpenTelemetry tracing (console, OTLP, GCP Trace) | |
For full details see:
- Full Application Audit (v0.9.0) — complete feature documentation, architecture, and DevOps playbook
- 000-docs/ index — complete documentation inventory (60+ docs)
- V1 Blueprint — engineering architecture and module map
- ADR: LLM Plans, Not DXF Edits — core architectural decision
- Python 3.11 or 3.12
- pip
git clone https://github.com/jeremylongshore/cad-ai-agent.git
cd cad-ai-agent
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e ".[dev]"
pre-commit installDWG is not decoded natively by this package. Import and export require the separately installed ODA File Converter, which ezdxf detects at runtime. DXF and vector-PDF workflows do not require ODA.
Run this preflight before accepting DWG files:
python -c "from ezdxf.addons import odafc; print('DWG ready' if odafc.is_installed() else 'DWG unavailable: install ODA File Converter or export the drawing as DXF')"If the result is DWG unavailable, install ODA File Converter and make its executable discoverable to ezdxf, or set CAD_ODA_PATH. The planned cloud-conversion fallback is not implemented; the application will fail clearly instead of uploading the drawing elsewhere.
# All tests (~4700 tests across 10 tiers)
make test
# With coverage (65% threshold)
make test-cov
# By tier
make test-unit # ~3600 unit tests
make test-integration # ~100 integration tests
make test-web # ~420 web API tests
make test-e2e # ~33 end-to-end tests
# Eval scorecard (intent classification accuracy)
make scorecard # mock mode
# Smoke test
make smokemake checkThis runs: lint → format check → type check → tests → smoke test.
make lint # ruff check
make format # ruff format
make typecheck # mypy
make test # pytest
make smoke # smoke test script
make security # bandit + pip-auditmake run
# or: python -m cad_dxf_agent.appThis opens the PySide6 desktop window. You can:
- Click Open DXF to load a file
- Type a prompt (e.g., "Move the column east by 2 feet")
- Click Plan & Preview to see proposed changes
- Click Apply & Save As to save the edited DXF
The repo ships a full web UI — React + Vite frontend, FastAPI backend — that you run locally or self-host. There is no public hosted instance.
# Frontend (React + Vite)
cd web/frontend && npm run dev # http://localhost:3000
# Backend (FastAPI) — CAD_WEB_DEV_MODE=1 skips Firebase auth for local testing
CAD_WEB_DEV_MODE=1 uvicorn web.backend.main:app --port 8322The backend reuses the same pipeline as the CLI and desktop app, so configure CAD_LLM_PROVIDER the same way (see Bring Your Own LLM Provider). A deploy-web.yml workflow exists for deploying to Cloud Run + Firebase Hosting, but it is wired to its operator's cloud project — repoint it at your own before using it.
The default LLM provider is mock, which uses simple keyword matching to generate operations. This lets you test the entire pipeline offline:
# Set in .env or environment (this is the default):
export CAD_LLM_PROVIDER=mock
# Run the smoke test:
python scripts/smoke_test.pyThe mock provider responds to keywords like "move", "delete", "text", "rename" in your prompt.
The default mock provider only keyword-matches — it's for exercising the pipeline offline, not for real results. The LLM is fully pluggable: the planner never sees raw DXF, it only returns a structured ChangeSet, so any model that can follow that contract works. Point CAD_LLM_PROVIDER at your own provider — no fork required.
The engine is model-agnostic. You bring the provider, model, endpoint, and credentials; the engine only accepts and validates the structured ChangeSet. For portability, providers may use these shared environment variables:
| Variable | Meaning |
|---|---|
CAD_LLM_MODEL |
Model identifier (optional) |
CAD_LLM_API_KEY |
Provider API key (optional; never logged) |
CAD_LLM_BASE_URL |
OpenAI-compatible or provider API endpoint (optional) |
Provider-specific variables are also supported. CAD_GEMINI_API_KEY takes precedence over the generic key when requested through get_api_key("gemini-key").
Subclass PlannerProvider (src/cad_dxf_agent/llm/providers.py):
# my_providers.py
from cad_dxf_agent.llm.providers import PlannerProvider
from cad_dxf_agent.models.ops_schema import ChangeSet
class MyProvider(PlannerProvider):
@property
def name(self) -> str:
return "my-provider"
def plan(
self,
prompt: str,
drawing_context: dict,
conversation_history: list[dict] | None = None,
) -> ChangeSet:
# Call your model and return a validated ChangeSet of structured
# operations. The engine validates and applies them — the model never
# touches DXF. Read your own keys / endpoint / model from the
# environment in __init__.
...The contract is small (plan() + a name) and the provider must be constructible with no arguments.
CAD_LLM_PROVIDER accepts a dotted import path — package.module:ClassName:
export CAD_LLM_PROVIDER=my_providers:MyProvider
python -m cad_dxf_agent.appThe class is imported and validated as a PlannerProvider at startup. A bad path, or a class that isn't a PlannerProvider, fails loudly — it never silently degrades to mock and produces keyword-matched nonsense.
The
src/cad_dxf_agent/llm/package contains existing provider implementations you can copy as a starting point.
Both paths use tool-use with vision — the planner analyzes DXF renders and, for complex multi-step requests, runs an iterative agent loop (up to 10 turns) over 20+ query/edit tools. The mock provider remains available for offline pipeline testing.
When enabled (default), the tool inserts a revision note on the AI_REV_NOTES layer after applying edits. Notes are generated deterministically from the operations — never from freeform LLM output.
Examples:
REV 8 - Column shiftREV 8 - Moved entity east 2'-0"REV 5 - Updated text to 'New Label'; Deleted entity
Configure via environment variables:
CAD_REVISION_NOTES_ENABLED=true # true/false
CAD_REVISION_NOTES_LAYER=AI_REV_NOTESProtected layers (TITLE, TITLEBLOCK, SEAL, REVISION) are never modified. The real title block revision table is out of scope for V1.
Compare two DXF versions, review structural changes, and apply approved edits to produce a new drawing.
cad-revision <command> [options]| Command | What it does |
|---|---|
cad-revision diff |
Compare two DXFs and output a changelog |
cad-revision align |
Check/preview alignment transform only |
cad-revision dry-run |
Full pipeline without writing any files |
cad-revision apply |
Apply approved changes to a new DXF |
cad-revision bundle |
Produce a zip with DXF + overlay + changelog |
cad-revision explain |
Human-readable explanation of changes |
Quick example:
# Compare and generate changelog
cad-revision diff master.dxf revision.dxf --output-dir ./out
# Full bundle with all changes auto-approved
cad-revision bundle master.dxf revision.dxf --output-dir ./bundle --approve-allGlobal flags: --json, --verbose, --version
Exit codes: 0 = no changes, 1 = changes found, 2 = error
Manual control points (for drawings with large coordinate offsets):
cad-revision diff master.dxf revision.dxf \
--control-points "100,200:105,205" "300,400:305,405"The web revision workflow is a 5-step wizard in the Compare tab:
- Upload — Upload a revision DXF to compare against the current master
- Align — Automatic alignment (or manual control points if confidence is low)
- Review — Approve or reject each detected change
- Apply — Apply approved changes to produce a new DXF
- Download — Download a bundle (.zip) containing the updated master, diff overlay, changelog, and metadata
- Units mismatch — Master in inches, revision in mm → low alignment confidence. Convert both files to matching units first.
- Partial revisions — Revision contains only a subset of the drawing → alignment may fail. Use manual control points (
--control-pointsin CLI, or the manual alignment UI in the web app). - Xrefs and dynamic blocks — Not yet supported. These are detected and skipped with a warning message.
By default, these layers are protected and cannot be edited:
TITLETITLEBLOCKSEALREVISION
Customize via environment variable:
CAD_PROTECTED_LAYERS=TITLE,TITLEBLOCK,SEAL,REVISION,CUSTOM_LAYERAny operation targeting an entity on a protected layer will be blocked by the validator.
Optional distributed tracing for pipeline performance insights. Off by default, CI-safe (no network calls when disabled).
# Install OTel extras
pip install -e ".[otel]"
# Enable console exporter (prints spans to stdout)
OTEL_ENABLED=1 python scripts/smoke_test.py
# Send to an OTLP collector (Jaeger, Grafana Tempo, etc.)
OTEL_ENABLED=1 OTEL_EXPORTER=otlp OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 python scripts/smoke_test.py
# GCP Cloud Trace (production)
OTEL_ENABLED=1 OTEL_EXPORTER=gcp-trace python scripts/smoke_test.py| Span Name | Attributes |
|---|---|
cad.load_dxf |
cad.file.name, cad.entities.count, cad.layers.count |
cad.build_context |
cad.entities.count |
cad.run_planner |
cad.mode, cad.ops.count |
cad.validate |
cad.ops.count, cad.validation.valid, cad.validation.blockers |
cad.apply_changeset |
cad.ops.count, cad.ops.success_count |
cad.save |
cad.save.output_basename |
cad.revision_note |
cad.revision.layer |
Privacy: No full file paths, no drawing text content, no API keys are ever included in span attributes.
| Variable | Default | Purpose |
|---|---|---|
OTEL_ENABLED |
(unset) | Enable tracing (1, true, yes) |
OTEL_EXPORTER |
console |
Exporter type: console, otlp, or gcp-trace |
OTEL_EXPORTER_OTLP_ENDPOINT |
(unset) | OTLP collector URL |
User Prompt
→ ObjectiveClassifier (2-axis: RequestClass × ObjectiveTag)
→ StrategyRegistry (maps classification → StagePipelineDefinition)
→ StageExecutor (runs ordered stages: deterministic + LLM)
→ ResponseBuilder (PlatformResponse envelope)
For edit requests, the stage pipeline includes:
Planner → ChangeSet → Validator → Preview → EditEngine → Save-As DXF + RevisionNotes
For analysis requests (compliance, health, takeoff, summary, RFI, zones), the pipeline runs deterministic extractors without the edit flow.
Every prompt is classified on two independent axes:
- RequestClass — what the user wants done:
edit,analyze,compare,query,generate - ObjectiveTag — why they want it:
compliance,coordination,documentation,estimation,quality,general
The StrategyRegistry maps each (RequestClass, ObjectiveTag) pair to a StagePipelineDefinition — an ordered list of StageHandler implementations.
For complex requests, the AgentProvider runs an iterative tool-use loop:
- Sends prompt + drawing context + tool definitions to the LLM provider
- The provider returns tool calls (query tools: list entities, find by layer; edit tools: move, delete, add)
ToolExecutordispatches each call, enforcing protected-layer rules- Results feed back to the provider for the next iteration (max 10 turns)
- Final ChangeSet extracted from accumulated tool calls
20+ tools split into query (read-only) and edit (state-changing) categories.
cad-dxf-agent/
src/cad_dxf_agent/
app.py # Desktop entry point
settings.py # Env-based configuration
otel.py # OpenTelemetry bootstrap
core/ # 41 modules — DXF I/O, validation, editing, analysis
llm/ # 22 modules — intent classification, planning, agent loop
models/ # 30 Pydantic schemas
cli/ # Revision CLI (cad-revision)
ui/ # PySide6 desktop UI
web/
frontend/ # React + Vite SPA (Firebase Hosting)
backend/ # FastAPI on Cloud Run (20+ endpoints)
tests/
unit/ # ~3600 unit tests
integration/ # ~100 integration tests
web/ # ~420 web API tests
eval/ # ~240 eval scorecard tests
e2e/ # ~33 end-to-end tests
benchmark/ # ~19 performance benchmarks
gui/ # ~10 PySide6 UI tests
property/ # ~7 fuzz/property tests
smoke/ # ~7 smoke tests
scripts/
smoke_test.py # Standalone smoke test
000-docs/ # All project docs (60+ files)
All docs live in 000-docs/ using flat chronological filing.
Key references:
- V1 Blueprint — architecture, module map, scope
- ADR: Local-First Architecture
- ADR: LLM Plans, Not DXF Edits
- ADR: AI Revision Notes on Safe Layer
Apache-2.0 — see LICENSE.