Strengthen pintable layout_rules skill and refresh empty extractions.
Treat PCB/application layout guidance as a first-class extract, keep those pages in PDF trim, and re-run IC extraction once when rules are still empty under older model_version. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -4,6 +4,8 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from packaging.version import Version
|
||||
|
||||
KNOWN_KINDS = frozenset({"decoupling_proximity", "thermal_via", "keepout", "length_match"})
|
||||
|
||||
|
||||
@@ -20,6 +22,37 @@ def _num(v: Any) -> float | None:
|
||||
return None
|
||||
|
||||
|
||||
def has_any_layout_rule(raw: object) -> bool:
|
||||
"""True when extraction already produced at least one structured rule."""
|
||||
if not isinstance(raw, list):
|
||||
return False
|
||||
for row in raw:
|
||||
if isinstance(row, dict) and str(row.get("kind") or "").strip() in KNOWN_KINDS:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def needs_layout_rules_refresh(
|
||||
data: dict,
|
||||
*,
|
||||
min_scan_version: str,
|
||||
) -> bool:
|
||||
"""True when layout_rules are empty and the extract predates the scan version.
|
||||
|
||||
After a successful extract at ``min_scan_version`` or newer, an empty
|
||||
``layout_rules`` list means the datasheet had no guidance — do not loop.
|
||||
"""
|
||||
if has_any_layout_rule(data.get("layout_rules")):
|
||||
return False
|
||||
ver = str(data.get("model_version") or "0.0.0")
|
||||
if not min_scan_version or min_scan_version == "0.0.0":
|
||||
return False
|
||||
try:
|
||||
return Version(ver) < Version(min_scan_version)
|
||||
except Exception:
|
||||
return True
|
||||
|
||||
|
||||
def validate_layout_rules(raw: list | None) -> tuple[list[dict], list[str]]:
|
||||
"""Return (normalized rows, errors). Empty list is a valid skip."""
|
||||
if not raw:
|
||||
|
||||
@@ -135,8 +135,36 @@ PINTABLE_TOOL = {
|
||||
},
|
||||
"layout_rules": {
|
||||
"type": "array",
|
||||
"description": "Optional PCB layout constraints from typical-application pages. kind must be decoupling_proximity, thermal_via, keepout, or length_match. max_distance_mm only if the PDF states a number — never invent 3 mm or 3W.",
|
||||
"items": {"type": "object"},
|
||||
"description": (
|
||||
"PCB layout constraints from typical-application / PCB layout pages. "
|
||||
"kind: decoupling_proximity | thermal_via | keepout | length_match. "
|
||||
"Fields: pin, cap_value_hint, max_distance_mm (ONLY if the PDF states a "
|
||||
"number — never invent 3 mm/JEDEC), same_layer (bool), min_via_count, "
|
||||
"net_class, note, source_page. Empty array if the PDF has no layout guidance."
|
||||
),
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"decoupling_proximity",
|
||||
"thermal_via",
|
||||
"keepout",
|
||||
"length_match",
|
||||
],
|
||||
},
|
||||
"pin": {"type": ["string", "null"]},
|
||||
"cap_value_hint": {"type": ["string", "null"]},
|
||||
"max_distance_mm": {"type": ["number", "null"]},
|
||||
"same_layer": {"type": ["boolean", "null"]},
|
||||
"min_via_count": {"type": ["integer", "null"]},
|
||||
"net_class": {"type": ["string", "null"]},
|
||||
"note": {"type": ["string", "null"]},
|
||||
"source_page": {"type": ["integer", "null"]},
|
||||
},
|
||||
"required": ["kind"],
|
||||
},
|
||||
},
|
||||
},
|
||||
"required": ["component_subtype", "component_subtype_description", "package_info", "pintable"],
|
||||
@@ -257,11 +285,17 @@ _MAX_PDF_PAGES = 120
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# Keywords used to find relevant pages for each extraction stage.
|
||||
# Include PCB / typical-application pages so layout_rules can be extracted
|
||||
# when large datasheets are trimmed to ≤_MAX_PDF_PAGES.
|
||||
_PINTABLE_KEYWORDS = re.compile(
|
||||
r"pin\s*(out|diagram|configuration|description|assignment|function|name|table|map)"
|
||||
r"|ball\s*map|package\s*(pin|drawing|outline)|signal\s+description"
|
||||
r"|absolute\s+maximum|recommended\s+operating|electrical\s+characteristics"
|
||||
r"|ordering\s+information|device\s+information",
|
||||
r"|ordering\s+information|device\s+information"
|
||||
r"|pcb\s+layout|layout\s+(guideline|recommendation|consideration|hint)"
|
||||
r"|typical\s+application|application\s+(circuit|schematic|information|note)"
|
||||
r"|reference\s+design|decoupling|bypass\s+capacitor|thermal\s+via"
|
||||
r"|land\s+pattern|keep[\s\-]?out|place\s+(close|near|within)",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
@@ -596,7 +630,12 @@ async def extract_pintable(
|
||||
skill_name="extract-pintable",
|
||||
model=model,
|
||||
system=system,
|
||||
user_text=f"Extract pin table and package info for MPN: {mpn}",
|
||||
user_text=(
|
||||
f"Extract pin table, package info, absolute maximum ratings, "
|
||||
f"and layout_rules (scan PCB layout / typical application / "
|
||||
f"thermal pages; max_distance_mm only if the PDF states mm) "
|
||||
f"for MPN: {mpn}"
|
||||
),
|
||||
pdf_path=trimmed,
|
||||
output_tool=_to_tool(PINTABLE_TOOL),
|
||||
)
|
||||
|
||||
@@ -762,6 +762,10 @@ async def _stage_ic_extraction(ctx: PipelineContext) -> None:
|
||||
extracted_dir = ctx.ws.local_path("extracted")
|
||||
|
||||
# Pre-categorize: workspace cache, library cache, or needs extraction
|
||||
from backend.config import settings as app_settings
|
||||
from backend.pinscopex.layout_rules import needs_layout_rules_refresh
|
||||
|
||||
layout_scan_ver = app_settings.get_default_model_version()
|
||||
_ic_cache: dict[str, tuple] = {}
|
||||
_ic_new_count = 0
|
||||
for mpn in ctx.ic_mpns:
|
||||
@@ -771,13 +775,27 @@ async def _stage_ic_extraction(ctx: PipelineContext) -> None:
|
||||
existing = json.loads(json_path.read_text())
|
||||
if existing.get("pintable"):
|
||||
ws_ver = existing.get("model_version", "0.0.0")
|
||||
if not settings_svc.version_is_stale(ws_ver, ctx.min_ver):
|
||||
if (
|
||||
not settings_svc.version_is_stale(ws_ver, ctx.min_ver)
|
||||
and not needs_layout_rules_refresh(
|
||||
existing, min_scan_version=layout_scan_ver,
|
||||
)
|
||||
):
|
||||
_ic_cache[mpn] = ("workspace",)
|
||||
continue
|
||||
lib_key = proj_svc.library_has_extraction(ctx.storage, mpn, min_version=ctx.min_ver)
|
||||
if lib_key:
|
||||
_ic_cache[mpn] = ("library", lib_key)
|
||||
continue
|
||||
# Library hit may still lack layout_rules under the current skill.
|
||||
try:
|
||||
lib_payload = ctx.storage.read_json(lib_key)
|
||||
except Exception:
|
||||
lib_payload = {}
|
||||
if not needs_layout_rules_refresh(
|
||||
lib_payload if isinstance(lib_payload, dict) else {},
|
||||
min_scan_version=layout_scan_ver,
|
||||
):
|
||||
_ic_cache[mpn] = ("library", lib_key)
|
||||
continue
|
||||
_ic_new_count += 1
|
||||
|
||||
broker.publish(ctx.project_id, "step_update",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"default_model_version": "1.9.0",
|
||||
"default_model_version": "1.10.0",
|
||||
"extract-pintable": {
|
||||
"skill_id": "skill_01VMWPZuvuZAe4LmLbmsNWNY",
|
||||
"latest_version": "1784798970179642",
|
||||
|
||||
Reference in New Issue
Block a user