Adapt Pinscope to DeepSeek, auto datasheets, and a shared library.

Based on manvalan/pinscope main. Default LLM is DeepSeek with local
skills and PDF ingest. Datasheets are fetched from LCSC/TI, stored in
the component library, and review extracts abs-max with a deeper
checklist. Adds scripts/update-pinscope.sh for the production host.
This commit is contained in:
Cursor Agent
2026-08-27 23:06:23 +00:00
parent ab1c5b081c
commit 48246f31bd
53 changed files with 3005 additions and 314 deletions
+78 -8
View File
@@ -1,9 +1,12 @@
"""Async datasheet extraction using Claude API.
"""Async datasheet extraction using the configured LLM provider.
Ports the extraction steps from run_pipeline.py to async:
- extract_pintable: Pin table + package info + taxonomy assignment
- extract_pattern: Passive MPN pattern
- extract_specs: Component specs (discrete, connectors, crystals, etc.)
Skills (SKILL.md + validate.py) run locally for DeepSeek/Gemini. Anthropic
can still use Console Skills when a skill_id is in skills_manifest.json.
"""
from __future__ import annotations
@@ -55,7 +58,7 @@ from backend.services.llm import (
PINTABLE_TOOL = {
"name": "save_pintable",
"description": "Save the extracted pin table, package info, and component subtype.",
"description": "Save the extracted pin table, package info, absolute-maximum ratings, and component subtype.",
"input_schema": {
"type": "object",
"properties": {
@@ -94,6 +97,31 @@ PINTABLE_TOOL = {
"required": ["number", "name"],
},
},
"absolute_maximum_ratings": {
"type": "array",
"description": (
"Rows from the Absolute Maximum Ratings table: supplies, "
"pin voltages, current, temperature. Omit recommended-"
"operating values. Empty array if the table is unreadable."
),
"items": {
"type": "object",
"properties": {
"parameter": {
"type": "string",
"description": "As printed, e.g. 'VCC', 'VIN', 'Storage temperature'",
},
"min": {"type": ["number", "null"]},
"max": {"type": ["number", "null"]},
"unit": {"type": "string", "description": "V, mA, °C, …"},
"source_page": {
"type": "integer",
"description": "1-based datasheet page of this row",
},
},
"required": ["parameter", "unit", "source_page"],
},
},
},
"required": ["component_subtype", "component_subtype_description", "package_info", "pintable"],
},
@@ -208,14 +236,16 @@ SPECS_TOOL = {
# ---------------------------------------------------------------------------
_MAX_PDF_PAGES = 90
_MAX_PDF_PAGES = 120
log = logging.getLogger(__name__)
# Keywords used to find relevant pages for each extraction stage.
_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"|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",
re.IGNORECASE,
)
@@ -280,6 +310,44 @@ def _to_tool(d: dict) -> ToolSchema:
)
def _coerce_abs_max(raw: object) -> list[dict]:
"""Keep well-formed abs-max rows; drop garbage rather than failing extraction."""
if not isinstance(raw, list):
return []
out: list[dict] = []
for row in raw:
if not isinstance(row, dict):
continue
parameter = str(row.get("parameter") or "").strip()
unit = str(row.get("unit") or "").strip()
page = row.get("source_page")
if not parameter or not unit:
continue
try:
source_page = int(page)
except (TypeError, ValueError):
continue
if source_page < 1:
continue
def _num(v: object) -> float | None:
if v is None or v == "":
return None
try:
return float(v)
except (TypeError, ValueError):
return None
out.append({
"parameter": parameter,
"min": _num(row.get("min")),
"max": _num(row.get("max")),
"unit": unit,
"source_page": source_page,
})
return out
_GENERATE_SPECS_TOOL = {
"name": "save_specs_schema",
"description": "Save the standardized parameter schema for a component type.",
@@ -479,7 +547,7 @@ async def extract_pintable(
taxonomy = format_for_prompt("ic", tax_dir)
trimmed = _select_pages(pdf_path, _PINTABLE_KEYWORDS)
skill_id, version = settings.get_skill("extract-pintable")
skill_id, version = settings.get_skill_or_none("extract-pintable")
system = (
f"DYNAMIC CONTEXT FOR THIS EXTRACTION:\n"
f"MPN: {mpn}\n\n"
@@ -548,7 +616,9 @@ async def extract_pintable(
component_subtype=subtype,
package_info=result["package_info"],
pintable=result["pintable"],
absolute_maximum_ratings=[],
absolute_maximum_ratings=_coerce_abs_max(
result.get("absolute_maximum_ratings") or [],
),
rules=[],
)
@@ -576,7 +646,7 @@ async def extract_pattern(
tax_dir = taxonomy_dir or settings.taxonomy_dir
taxonomy = format_for_prompt("passive", tax_dir)
skill_id, version = settings.get_skill("extract-pattern")
skill_id, version = settings.get_skill_or_none("extract-pattern")
system = (
f"DYNAMIC CONTEXT FOR THIS EXTRACTION:\n\n"
f"EXISTING PASSIVE TAXONOMY SUBTYPES:\n{taxonomy}\n\n"
@@ -665,7 +735,7 @@ async def extract_specs(
subtypes_text = format_for_prompt(component_type, tax_dir)
specs_text = format_specs_for_prompt(component_type, tax_dir)
skill_id, version = settings.get_skill("extract-specs")
skill_id, version = settings.get_skill_or_none("extract-specs")
system = (
f"DYNAMIC CONTEXT FOR THIS EXTRACTION:\n"
f"MPN: {mpn}\n"