A domain-expert assistant for semiconductor fab/process and physical-verification engineers: answers questions over process specs, design-rule manuals, and EDA documentation, and drafts ICV/PXL-style rule-check pseudocode. Built on public PDK/EDA data so it's fully shareable, and designed to be re-pointed at private client documentation for real fab/EDA-vendor work.
β οΈ LLM-generated answers can make mistakes. PDKScribe grounds every answer in retrieved sources and runs a citation guard that flags weakly grounded answersneeds_human_review, but this is a heuristic, not a guarantee β always verify citations before acting on a design-rule value or a generated rule snippet, especially anything safety- or mask-critical.
query
β
βΌ
router.py LLM classifies into one of three routes
β (structured output + keyword-heuristic fallback)
βΌ
retriever.py hybrid retrieval: dense (Qdrant) + BM25, fused by
β reciprocal rank (LlamaIndex QueryFusionRetriever)
βΌ
generators/ conceptual_qa.py β grounded prose explanation
<route> rule_lookup.py β grounded numeric rule value
rule_snippet.py β validated Pydantic spec, then
deterministically rendered
ICV/PXL pseudocode
β
βΌ
guardrails.py citation guard: cross-checks cited source_ids
β against what was actually retrieved β
β needs_human_review flag + reason
βΌ
memory.py per-session conversation history (file-backed),
replayed into the next turn's prompt
- Router (
router.py): classifies a query intoconceptual_QA,rule_lookup, orsnippet_generationvia a structured LLM call (llm/structured.pyβ JSON validated against a Pydantic schema, one retry on failure); falls back to a keyword heuristic if the LLM call fails outright, so the very first pipeline step never hard-fails. - Retrieval (
retriever.py+ingest/pdk_loader.py): a small, real, permissively-licensed corpus (see Datasets below) is chunked and indexed into an embedded on-disk Qdrant collection (dense leg) plus a BM25 index over the same nodes (sparse leg);QueryFusionRetrievercombines both with reciprocal-rank fusion. No LLM call is needed for retrieval itself. - Generation:
conceptual_qa.py/rule_lookup.pyare prompted to cite every claim inline as[source_id];rule_snippet.pyonly asks the LLM for the semantic fields of a rule (schemas.RuleSnippetSpec) and deterministically renders the actual ICV/PXL syntax in code, so the emitted pseudocode is always well-formed regardless of the model's grasp of runset syntax. Pseudocode is explanatory only β never executed against a real EDA tool. - Citation guard (
guardrails.py): cross-references citedsource_ids against what was actually retrieved. A fabricated citation, low citation coverage, or a safety-critical route (rule_lookup/snippet_generation) producing zero citations all setneeds_human_review=Truewith a reason. - Memory (
memory.py): one JSON file per session underpdkscribe_sessions/, replayed as a "prior conversation" block so follow-ups ("and for metal2?") resolve correctly. - Serving:
api.py(FastAPI:POST /chat,GET /health) +app.py(Streamlit chat UI, calls the API overPDKSCRIBE_API_BASE_URL). - Cloud:
infra/azure/main.bicepprovisions an Azure Container App (runningapi.py) + an Azure Blob Storage container for the corpus, with a managed identity grantedStorage Blob Data Contributorsocloud/blob_store.pyneeds no stored secret. LLM backend in cloud mode isclaudeorazure_foundry(an Azure AI Foundry model deployment β Foundry endpoints are OpenAI-compatible, soazure_foundryreuses the same client plumbing as the local backends).
| Backend | Where | Notes |
|---|---|---|
vllm (default) |
Local | Any OpenAI-compatible vLLM server you run yourself |
ollama |
Local | Any Ollama server you run yourself (ollama serve) |
llamacpp |
Local | Any OpenAI-compatible llama.cpp server you run yourself |
claude |
Cloud | Anthropic API, requires PDKSCRIBE_ANTHROPIC_API_KEY |
azure_foundry |
Cloud | Azure AI Foundry model deployment, requires a base URL + API key |
All five expose the same LLMClient.generate(system_prompt, user_prompt)
interface (llm/client.py); structured outputs (router classification, rule
snippets) are layered on top by prompting for JSON and validating against a
Pydantic schema (llm/structured.py), so they're interchangeable regardless
of which one has native JSON/tool-calling support.
ingest/pdk_loader.py fetches and caches these on first run (offline-safe
afterward):
| Source | License | Content |
|---|---|---|
| SkyWater SKY130 PDK docs | Apache-2.0 | GDS layer reference + design-rule tables (metal1/2, via, licon, li, poly, nwell, diff/tap) |
| ASAP7 predictive PDK | BSD-3-Clause | 7nm FinFET process overview |
| arXiv abstracts | Open access | A few open-source-silicon / open-EDA papers, fetched live via the arXiv API |
unstructured partitions each cached file (CSV/Markdown) into elements,
which are re-joined into LlamaIndex Documents carrying source_id /
title / source_url metadata β the citation trail every retrieved chunk
and generated answer traces back to. If a source can't be fetched (no
network on first run), it's skipped with a warning rather than failing the
whole corpus build.
To point PDKScribe at private client documentation instead, add sources to
ingest/pdk_loader.py (or drop files directly into PDKSCRIBE_CORPUS_DIR)
following the same CorpusSource pattern.
cd PDKScribe
python3.12 -m venv venv && source venv/bin/activate
pip install -r requirements.txt
cp .env.example .env # defaults work for local vLLM modeunstructured[md] needs the system package libmagic1 for reliable
file-type detection: sudo apt-get install -y libmagic1 (it still works
without it, just noisier).
This project was developed and tested against Python 3.12 β the retrieval stack (
llama-index-vector-stores-qdrant,qdrant-client,sentence-transformers) lagged behind Python 3.14 wheel availability at the time of writing. Usepython3.12 -m venv venveven if a newer interpreter is also installed.
start_pdkscribe.sh assumes the backend is already running β it only
checks reachability and warns if it isn't. Start one first:
vllm serve Qwen/Qwen2.5-14B-Instruct-AWQ --port 8000 # vLLM (default backend)
# or: ollama serve (Ollama)
# or: your own llama.cpp server on port 8080 (llama.cpp)
# or nothing, if using --backend claude / --backend azure_foundry (just set the API key in .env)./scripts/start_pdkscribe.sh
# First run: downloads the corpus (~30s).Open http://localhost:8501 and ask something like "What's the minimum
metal1 spacing in SKY130?" or "Write a DRC check for poly-to-diff
spacing." Stop everything with ./scripts/stop_pdkscribe.sh (your LLM
server is left running β stop it separately if you no longer need it).
./scripts/start_pdkscribe.sh --backend ollama # after starting `ollama serve`
./scripts/start_pdkscribe.sh --backend llamacpp # after starting your llama.cpp serverpython -m pdkscribe.ingest.pdk_loadercd infra/azure
az deployment group create \
--resource-group <rg> \
--template-file main.bicep \
--parameters containerImage=<registry>/pdkscribe:latest \
llmBackend=claude anthropicApiKey=<key>This provisions a Container App running api.py, a Storage Account +
Blob container for the corpus, and a user-assigned managed identity granted
Storage Blob Data Contributor on that container (no storage secret needed
inside the app β cloud/blob_store.py authenticates via
DefaultAzureCredential). Set PDKSCRIBE_RUN_MODE=cloud and call
pdkscribe.cloud.blob_store.upload_corpus() / download_corpus() to sync
the corpus to/from Blob Storage.
To use an Azure AI Foundry model deployment instead of Claude, deploy the
model via the Foundry portal/CLI first (not a Bicep resource this template
manages), then pass llmBackend=azure_foundry, azureFoundryBaseUrl, and
azureFoundryApiKey.
This template is written to be deploy-ready but has not been applied against a real Azure subscription as part of this repository.
source venv/bin/activate
pytest tests/ -vUnit tests use a stub LLM client (tests/fixtures/stub_llm.py) and a fake
retriever β no network, GPU, or vector index build required. This covers
the router (heuristic + structured-output + fallback), the citation guard,
the rule-snippet renderer, conversation memory, and the full agent pipeline
end-to-end with mocked LLM/retrieval. tests/test_ingest.py exercises the
corpus fetch/cache logic with requests.get monkeypatched, so it passes
identically with or without internet access.
tutorial/ has a set of Jupyter notebooks that walk through the codebase
module by module β corpus ingestion, hybrid retrieval, schemas & LLM
backends, the router, generators & citation guard, the agent pipeline &
memory, and the API/UI/cloud layer. See tutorial/README.md to get
started.
pdkscribe/
config.py Settings (pydantic-settings): run mode, LLM backend, Qdrant, citation guard
schemas.py Pydantic models: RouteClassification, RuleSnippet(Spec), Citation, AgentAnswer
llm/client.py LLMClient ABC + VLLMClient/OllamaClient/LlamaCppClient/ClaudeClient/AzureFoundryClient
llm/structured.py generate_structured(): JSON-mode prompt + Pydantic validation + 1 retry
router.py classify_query(): structured LLM output, keyword-heuristic fallback
retriever.py hybrid BM25 + dense retrieval (LlamaIndex + Qdrant + sentence-transformers)
ingest/pdk_loader.py fetches/caches/parses the public corpus (unstructured)
generators/ conceptual_qa.py, rule_lookup.py, rule_snippet.py
guardrails.py citation guard + needs_human_review
memory.py file-backed per-session conversation history
agent.py orchestrates router β retrieve β generate β guard β memory
api.py FastAPI: POST /chat, GET /health
app.py Streamlit chat UI
cloud/blob_store.py Azure Blob corpus sync (cloud mode)
infra/azure/main.bicep Container App + Blob Storage + managed identity
scripts/ start_pdkscribe.sh, stop_pdkscribe.sh
tests/ pytest suite + fixtures (stub LLM, no network/GPU needed)
tutorial/ Jupyter notebooks walking through the codebase module by module
(data/corpus/, data/qdrant_storage/, and pdkscribe_sessions/ are gitignored runtime data, created on first run.)
Apache License 2.0 β see LICENSE.