Ship DeepSeek roadmap P0-5/P2 integrations.
Smoke simple_project offline, reviewer shortest_path, library promotion gate, PDF drop logs, and per-stage cache hit-rate helper — without touching Layout placement packing. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
"""Shared-library promotion gates for extracted IC JSON."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
|
||||
def pintable_checksum(pintable: list[Any]) -> str:
|
||||
"""Stable hash of pin number+name pairs (order-independent)."""
|
||||
rows: list[tuple[str, str]] = []
|
||||
for pin in pintable or []:
|
||||
if isinstance(pin, dict):
|
||||
num = str(pin.get("number") or "").strip()
|
||||
name = str(pin.get("name") or "").strip()
|
||||
else:
|
||||
num = str(getattr(pin, "number", "") or "").strip()
|
||||
name = str(getattr(pin, "name", "") or "").strip()
|
||||
if num:
|
||||
rows.append((num, name))
|
||||
payload = json.dumps(sorted(rows), separators=(",", ":"))
|
||||
return hashlib.sha256(payload.encode("utf-8")).hexdigest()[:16]
|
||||
|
||||
|
||||
def should_promote_extraction(data: dict) -> tuple[bool, str]:
|
||||
"""Return (ok, reason). Reject empty / tiny pintables from shared library."""
|
||||
pins = data.get("pintable") or []
|
||||
if not isinstance(pins, list) or len(pins) == 0:
|
||||
return False, "empty pintable"
|
||||
if len(pins) < 2:
|
||||
return False, "pintable has fewer than 2 pins"
|
||||
# Require at least one named pin so a number-only stub cannot poison the library.
|
||||
named = 0
|
||||
for pin in pins:
|
||||
name = pin.get("name") if isinstance(pin, dict) else getattr(pin, "name", None)
|
||||
if name and str(name).strip() and str(name).strip() != "~":
|
||||
named += 1
|
||||
if named == 0:
|
||||
return False, "pintable has no named pins"
|
||||
return True, pintable_checksum(pins)
|
||||
@@ -35,6 +35,12 @@ def extract_pdf_document_text(pdf_path: Path | str, *, max_chars: int) -> str:
|
||||
parts.append(f"--- page {i} ---\n{body}")
|
||||
blob = "\n\n".join(parts)
|
||||
if len(blob) > max_chars:
|
||||
# Count how many page markers survive the cut for observability.
|
||||
kept = blob[:max_chars].count("--- page ")
|
||||
log.info(
|
||||
"PDF text truncated: %s full=%d chars cap=%d kept_pages≈%d/%d",
|
||||
path.name, len(blob), max_chars, kept, n,
|
||||
)
|
||||
blob = blob[:max_chars] + "\n\n[truncated: remaining pages omitted]"
|
||||
return blob
|
||||
|
||||
|
||||
@@ -256,6 +256,104 @@ def get_net_for_pin(
|
||||
return f"Pin {pin}{pin_name} on {designator} -> {net_name} [{net.net_type.value}{voltage_str}]"
|
||||
|
||||
|
||||
def shortest_path(
|
||||
graph: DesignGraph,
|
||||
constraints_map: ConstraintsMap,
|
||||
designator_a: str,
|
||||
pin_a: str,
|
||||
designator_b: str,
|
||||
pin_b: str,
|
||||
*,
|
||||
max_hops: int = 12,
|
||||
) -> str:
|
||||
"""BFS through the bipartite graph from A.pin to B.pin.
|
||||
|
||||
Hops alternate component→net→component. Returns the hop list or a
|
||||
clear miss message. Caps depth so the reviewer cannot explode memory
|
||||
on dense power nets.
|
||||
"""
|
||||
a = graph.components.get(designator_a)
|
||||
b = graph.components.get(designator_b)
|
||||
if not a:
|
||||
return f"Component '{designator_a}' not found."
|
||||
if not b:
|
||||
return f"Component '{designator_b}' not found."
|
||||
|
||||
net_a = a.pins.get(str(pin_a))
|
||||
net_b = b.pins.get(str(pin_b))
|
||||
if not net_a:
|
||||
return f"Pin {pin_a} on {designator_a} is not connected in the netlist."
|
||||
if not net_b:
|
||||
return f"Pin {pin_b} on {designator_b} is not connected in the netlist."
|
||||
|
||||
if designator_a == designator_b and str(pin_a) == str(pin_b):
|
||||
return f"Same endpoint: {designator_a}.{pin_a} on {net_a}."
|
||||
|
||||
if net_a == net_b:
|
||||
return (
|
||||
f"Direct (same net): {designator_a}.{pin_a} —[{net_a}]— "
|
||||
f"{designator_b}.{pin_b}"
|
||||
)
|
||||
|
||||
# BFS on component nodes; edges are nets shared between components.
|
||||
from collections import deque
|
||||
|
||||
start = designator_a
|
||||
goal = designator_b
|
||||
queue: deque[str] = deque([start])
|
||||
# prev[ref] = (previous_ref, via_net)
|
||||
prev: dict[str, tuple[str, str] | None] = {start: None}
|
||||
hops = 0
|
||||
found = False
|
||||
while queue and hops < max_hops:
|
||||
hops += 1
|
||||
for _ in range(len(queue)):
|
||||
cur = queue.popleft()
|
||||
for net_name, others in graph.neighbors(cur).items():
|
||||
for other in others:
|
||||
if other in prev:
|
||||
continue
|
||||
prev[other] = (cur, net_name)
|
||||
if other == goal:
|
||||
found = True
|
||||
queue.clear()
|
||||
break
|
||||
queue.append(other)
|
||||
if found:
|
||||
break
|
||||
if found:
|
||||
break
|
||||
|
||||
if not found or goal not in prev:
|
||||
return (
|
||||
f"No path within {max_hops} hops from "
|
||||
f"{designator_a}.{pin_a} ({net_a}) to "
|
||||
f"{designator_b}.{pin_b} ({net_b})."
|
||||
)
|
||||
|
||||
# Reconstruct component chain, then decorate endpoints with pins.
|
||||
chain_refs: list[str] = []
|
||||
via_nets: list[str] = []
|
||||
node = goal
|
||||
while node != start:
|
||||
chain_refs.append(node)
|
||||
parent, via = prev[node] # type: ignore[misc]
|
||||
via_nets.append(via)
|
||||
node = parent
|
||||
chain_refs.append(start)
|
||||
chain_refs.reverse()
|
||||
via_nets.reverse()
|
||||
|
||||
parts: list[str] = [f"{designator_a}.{pin_a}"]
|
||||
for i, via in enumerate(via_nets):
|
||||
nxt = chain_refs[i + 1]
|
||||
if nxt == designator_b:
|
||||
parts.append(f"—[{via}]— {designator_b}.{pin_b}")
|
||||
else:
|
||||
parts.append(f"—[{via}]— {nxt}")
|
||||
return f"Path ({len(via_nets)} hop(s)): " + " ".join(parts)
|
||||
|
||||
|
||||
def get_pintable(
|
||||
graph: DesignGraph,
|
||||
constraints_map: ConstraintsMap,
|
||||
@@ -590,6 +688,38 @@ GET_NET_FOR_PIN_SCHEMA = {
|
||||
},
|
||||
}
|
||||
|
||||
SHORTEST_PATH_SCHEMA = {
|
||||
"name": "shortest_path",
|
||||
"description": (
|
||||
"Find the shortest hop path through the netlist between two pins "
|
||||
"(component.pin → nets → components). Use to verify whether two "
|
||||
"pins share a rail path, or how a signal reaches another IC, "
|
||||
"instead of guessing from neighborhood context."
|
||||
),
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"designator_a": {
|
||||
"type": "string",
|
||||
"description": "Start component reference, e.g. 'U1'",
|
||||
},
|
||||
"pin_a": {
|
||||
"type": "string",
|
||||
"description": "Start pin number, e.g. '12'",
|
||||
},
|
||||
"designator_b": {
|
||||
"type": "string",
|
||||
"description": "End component reference, e.g. 'U3'",
|
||||
},
|
||||
"pin_b": {
|
||||
"type": "string",
|
||||
"description": "End pin number, e.g. '5'",
|
||||
},
|
||||
},
|
||||
"required": ["designator_a", "pin_a", "designator_b", "pin_b"],
|
||||
},
|
||||
}
|
||||
|
||||
GET_PINTABLE_SCHEMA = {
|
||||
"name": "get_pintable",
|
||||
"description": (
|
||||
@@ -727,6 +857,7 @@ GET_DATASHEET_EXCERPT_SCHEMA = {
|
||||
GRAPH_TOOLS = [
|
||||
FIND_CONNECTED_COMPONENTS_SCHEMA,
|
||||
GET_NET_FOR_PIN_SCHEMA,
|
||||
SHORTEST_PATH_SCHEMA,
|
||||
GET_PINTABLE_SCHEMA,
|
||||
GET_DATASHEET_EXCERPT_SCHEMA,
|
||||
]
|
||||
@@ -770,6 +901,17 @@ def execute_tool(
|
||||
),
|
||||
None,
|
||||
)
|
||||
if tool_name == "shortest_path":
|
||||
return (
|
||||
shortest_path(
|
||||
graph, constraints_map,
|
||||
tool_input["designator_a"],
|
||||
tool_input["pin_a"],
|
||||
tool_input["designator_b"],
|
||||
tool_input["pin_b"],
|
||||
),
|
||||
None,
|
||||
)
|
||||
if tool_name == "get_pintable":
|
||||
return (
|
||||
get_pintable(
|
||||
|
||||
@@ -104,3 +104,27 @@ class ApiLogger:
|
||||
|
||||
key = f"{project_prefix(user_id, project_id)}/api_logs.jsonl"
|
||||
storage.write_text(key, text)
|
||||
|
||||
|
||||
def cache_stats_by_stage(entries: list[dict]) -> dict[str, dict]:
|
||||
"""Roll up prompt-cache hit rate per pipeline stage.
|
||||
|
||||
Returns ``{stage: {calls, input_tokens, cache_read_tokens, hit_ratio}}``.
|
||||
``hit_ratio`` is cache_read / input when input > 0, else 0.
|
||||
"""
|
||||
out: dict[str, dict] = {}
|
||||
for e in entries:
|
||||
stage = str(e.get("stage") or "unknown")
|
||||
bucket = out.setdefault(
|
||||
stage,
|
||||
{"calls": 0, "input_tokens": 0, "cache_read_tokens": 0, "hit_ratio": 0.0},
|
||||
)
|
||||
bucket["calls"] += 1
|
||||
bucket["input_tokens"] += int(e.get("input_tokens") or 0)
|
||||
bucket["cache_read_tokens"] += int(e.get("cache_read_input_tokens") or 0)
|
||||
for bucket in out.values():
|
||||
inp = bucket["input_tokens"]
|
||||
bucket["hit_ratio"] = (
|
||||
round(bucket["cache_read_tokens"] / inp, 4) if inp else 0.0
|
||||
)
|
||||
return out
|
||||
|
||||
@@ -133,6 +133,16 @@ def render_pdf_page_jpegs(
|
||||
return []
|
||||
|
||||
try:
|
||||
total = len(doc)
|
||||
omitted = [i + 1 for i in range(total) if i not in set(page_indices)]
|
||||
if omitted:
|
||||
log.info(
|
||||
"PDF page images: %s rendering %d/%d pages; omitted e.g. %s",
|
||||
pdf_path.name,
|
||||
min(len(page_indices), max_pages),
|
||||
total,
|
||||
omitted[:12],
|
||||
)
|
||||
matrix = fitz.Matrix(zoom, zoom)
|
||||
for i in page_indices:
|
||||
if i < 0 or i >= len(doc):
|
||||
|
||||
@@ -839,10 +839,30 @@ async def _stage_ic_extraction(ctx: PipelineContext) -> None:
|
||||
api_logger=private,
|
||||
)
|
||||
|
||||
# Upload to storage, then copy to library
|
||||
# Upload to storage, then copy to library only if pintable is usable
|
||||
extracted_key = f"{ctx.ws.prefix}/extracted/{safe}.json"
|
||||
ctx.storage.upload_from_local(json_path, extracted_key)
|
||||
proj_svc.save_to_library(ctx.storage, extracted_key, "extracted", f"{safe}.json")
|
||||
try:
|
||||
from backend.pinscopex.library_gate import should_promote_extraction
|
||||
|
||||
payload = json.loads(json_path.read_text(encoding="utf-8"))
|
||||
ok, reason = should_promote_extraction(payload)
|
||||
if ok:
|
||||
proj_svc.save_to_library(
|
||||
ctx.storage, extracted_key, "extracted", f"{safe}.json",
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
"Skipping library promote for %s: %s (kept project-local)",
|
||||
mpn, reason,
|
||||
)
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"Library gate failed for %s — promoting anyway", mpn,
|
||||
)
|
||||
proj_svc.save_to_library(
|
||||
ctx.storage, extracted_key, "extracted", f"{safe}.json",
|
||||
)
|
||||
|
||||
# Upload source datasheet PDF to library (content-addressed)
|
||||
store_datasheet(ctx.storage, pdf_path, mpn)
|
||||
|
||||
Reference in New Issue
Block a user