LLM Classification¶
pyGAEB can enrich each item with a semantic construction element type using any LLM provider via LiteLLM. Classification is always opt-in — the core parser never requires LLM access.
Setup¶
Install the LLM extras:
Set your provider's API key:
Basic Classification¶
from pygaeb import GAEBParser, LLMClassifier
doc = GAEBParser.parse("tender.X83")
classifier = LLMClassifier(model="anthropic/claude-sonnet-4-6")
# Async
await classifier.enrich(doc)
# Or synchronous
classifier.enrich_sync(doc)
After enrichment, each item has a classification attribute:
for item in doc.iter_items():
c = item.classification
if c:
print(f"{item.short_text}: {c.trade} / {c.element_type} / {c.sub_type}")
print(f" confidence={c.confidence:.2f} flag={c.flag.value}")
Tip
doc.iter_items() works for both procurement (X80–X89) and trade (X93–X97) documents. For procurement-only code you can still use doc.award.boq.iter_items().
Cost Estimation¶
Before running classification, check the estimated cost:
estimate = await classifier.estimate_cost(doc)
print(f"Items to classify: {estimate.items_to_classify}")
print(f"Estimated cost: ${estimate.estimated_cost_usd:.2f}")
print(f"Cached (free): {estimate.cached_items}")
Taxonomy¶
Classification uses a three-level hierarchy: Trade > Element Type > Sub-Type.
| Trade | Element Types |
|---|---|
| Structural | Wall, Floor, Roof, Foundation, Column, Beam |
| Finishes | Door, Window, Ceiling, Cladding, Flooring |
| Roofing | Roof Covering, Insulation, Drainage, Flashing |
| MEP-Mechanical | Duct, Air Handling Unit, Fan, Diffuser |
| MEP-Electrical | Cable, Panel, Luminaire, Socket, Conduit |
| MEP-Plumbing | Pipe, Valve, Pump, Sanitary Fixture |
| Sitework | Excavation, Paving, Landscaping, Fence |
| Preliminaries | Site Setup, Scaffolding, Welfare, Temp Works |
| Other | Unclassifiable |
Each element type has further sub-types (e.g., Door has Single Door, Double Door, Fire Door, Sliding Door, Revolving Door).
Access the taxonomy programmatically:
from pygaeb.classifier.taxonomy import TAXONOMY, get_subtypes
subtypes = get_subtypes("Finishes", "Door")
# ["Single Door", "Double Door", "Fire Door", "Sliding Door", "Revolving Door"]
Confidence Flags¶
Each classification result carries a flag indicating the confidence level:
| Flag | Confidence | Meaning |
|---|---|---|
auto-classified |
>= 0.85 | High confidence — safe to use automatically |
needs-spot-check |
0.60–0.84 | Medium confidence — spot-check recommended |
needs-review |
< 0.60 | Low confidence — manual review needed |
llm-error |
N/A | LLM call failed — item was not classified |
manual-override |
N/A | Manually set by user (preserved across re-runs) |
from pygaeb.models.enums import ClassificationFlag
for item in doc.iter_items():
if item.classification and item.classification.flag == ClassificationFlag.NEEDS_REVIEW:
print(f"Review needed: {item.short_text}")
Manual Overrides¶
Override the LLM classification for specific items:
from pygaeb.models.item import ClassificationResult
from pygaeb.models.enums import ClassificationFlag
item = doc.award.boq.get_item("01.02.0030")
item.classification = ClassificationResult(
trade="Finishes",
element_type="Door",
sub_type="Fire Door",
confidence=1.0,
flag=ClassificationFlag.MANUAL_OVERRIDE,
)
Manual overrides are preserved when re-running classification — the classifier skips items flagged as MANUAL_OVERRIDE.
Supported LLM Providers¶
pyGAEB uses LiteLLM, so any supported provider works:
# Cloud APIs
LLMClassifier(model="anthropic/claude-sonnet-4-6")
LLMClassifier(model="gpt-4o")
LLMClassifier(model="gemini/gemini-1.5-pro")
# Enterprise endpoints
LLMClassifier(model="azure/gpt-4o")
LLMClassifier(model="bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0")
# Local (air-gapped)
LLMClassifier(model="ollama/llama3")
Progress Tracking¶
Monitor classification progress with a callback:
def on_progress(completed: int, total: int, current_label: str):
print(f"[{completed}/{total}] Classifying {current_label}...")
await classifier.enrich(doc, on_progress=on_progress)
Re-classification¶
By default, items with existing classification results are skipped (pulled from cache). To force re-classification:
Fallback Models¶
Specify fallback models in case the primary model fails:
classifier = LLMClassifier(
model="anthropic/claude-sonnet-4-6",
fallbacks=["gpt-4o", "ollama/llama3"],
)
If the primary model returns an error, pyGAEB tries each fallback in order before giving up.
Custom Taxonomy & Prompt¶
Override the built-in taxonomy and system prompt per classifier instance:
my_taxonomy = {
"Electrical": {"Cable Tray": ["Ladder", "Perforated"], "Panel": ["MCC", "DB"]},
"HVAC": {"AHU": ["Rooftop", "Indoor"], "Duct": ["Galvanised", "Flexible"]},
}
classifier = LLMClassifier(
model="gpt-4o",
taxonomy=my_taxonomy,
prompt_template="You are a specialist classifying MEP items...",
)
You can also register reusable prompt templates:
from pygaeb import register_prompt
register_prompt("mep-v1", "You are classifying MEP items...")
classifier = LLMClassifier(prompt_version="mep-v1")
See the Extensibility Guide for more details.
Concurrency¶
Classification runs items in parallel for speed:
# Default: 5 concurrent LLM calls
classifier = LLMClassifier(model="gpt-4o")
# Increase for faster throughput
from pygaeb import configure
configure(classifier_concurrency=20)
Caching¶
By default, classification results are cached in memory for the session. For persistent caching across runs, see the Caching Guide.