traceforge-pcb 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- traceforge/__init__.py +3 -0
- traceforge/__main__.py +3 -0
- traceforge/acp_server.py +570 -0
- traceforge/agent.py +680 -0
- traceforge/altium/__init__.py +6 -0
- traceforge/altium/cfb.py +70 -0
- traceforge/altium/cli.py +60 -0
- traceforge/altium/model.py +163 -0
- traceforge/altium/outputs.py +24 -0
- traceforge/altium/pcb.py +301 -0
- traceforge/altium/project.py +328 -0
- traceforge/altium/schematic.py +400 -0
- traceforge/altium/tools.py +166 -0
- traceforge/approval.py +219 -0
- traceforge/attachments.py +346 -0
- traceforge/background.py +253 -0
- traceforge/bench/__init__.py +5 -0
- traceforge/bench/adapters.py +337 -0
- traceforge/bench/checks.py +410 -0
- traceforge/bench/cli.py +121 -0
- traceforge/bench/design.py +439 -0
- traceforge/bench/kicad.py +99 -0
- traceforge/bench/partscheck.py +159 -0
- traceforge/bench/review.py +100 -0
- traceforge/bench/runner.py +226 -0
- traceforge/bench/score.py +204 -0
- traceforge/bench/tasks.py +102 -0
- traceforge/chat.py +1185 -0
- traceforge/checks.py +328 -0
- traceforge/checks_sides.py +118 -0
- traceforge/cli.py +2224 -0
- traceforge/clipboard.py +107 -0
- traceforge/config.py +299 -0
- traceforge/context.py +276 -0
- traceforge/design/__init__.py +1 -0
- traceforge/design/constraints.py +397 -0
- traceforge/design/generate.py +1789 -0
- traceforge/design/outline.py +905 -0
- traceforge/design/schematic_layout.py +932 -0
- traceforge/design/schematic_lint.py +329 -0
- traceforge/design/schematic_route.py +207 -0
- traceforge/design/sheets.py +854 -0
- traceforge/design/sides.py +328 -0
- traceforge/design/spec.py +462 -0
- traceforge/design/specsheet.py +2351 -0
- traceforge/design/specsheet_render.py +183 -0
- traceforge/desktop/__init__.py +109 -0
- traceforge/desktop/keys.py +90 -0
- traceforge/desktop/project.py +443 -0
- traceforge/desktop/review.py +20 -0
- traceforge/desktop/server.py +1277 -0
- traceforge/desktop/static/app.css +223 -0
- traceforge/desktop/static/app.js +683 -0
- traceforge/desktop/static/highlight.js +77 -0
- traceforge/desktop/static/index.html +89 -0
- traceforge/desktop/static/md.js +90 -0
- traceforge/desktop/static/viewer.js +213 -0
- traceforge/desktop/symbols.py +212 -0
- traceforge/desktop/update.py +65 -0
- traceforge/diagnostics.py +67 -0
- traceforge/errata.py +219 -0
- traceforge/fab.py +108 -0
- traceforge/fab_layers.py +44 -0
- traceforge/features/__init__.py +6 -0
- traceforge/features/candidates.py +117 -0
- traceforge/features/cli.py +200 -0
- traceforge/features/package.py +433 -0
- traceforge/features/revdiff.py +224 -0
- traceforge/features/sarif.py +145 -0
- traceforge/features/viewer.py +265 -0
- traceforge/firmware/__init__.py +3 -0
- traceforge/firmware/cli.py +174 -0
- traceforge/firmware/drivers.py +372 -0
- traceforge/firmware/drivers_data.py +256 -0
- traceforge/firmware/mcus.py +177 -0
- traceforge/firmware/netlist.py +55 -0
- traceforge/firmware/pinmap.py +296 -0
- traceforge/firmware/scaffold.py +466 -0
- traceforge/firmware/toolchain.py +262 -0
- traceforge/hooks.py +237 -0
- traceforge/kicad/__init__.py +0 -0
- traceforge/kicad/cli.py +519 -0
- traceforge/kicad/diffpair.py +174 -0
- traceforge/kicad/edgegeom.py +191 -0
- traceforge/kicad/edit.py +405 -0
- traceforge/kicad/escape.py +1410 -0
- traceforge/kicad/formats.py +1073 -0
- traceforge/kicad/impedance.py +320 -0
- traceforge/kicad/install.py +75 -0
- traceforge/kicad/libraries.py +490 -0
- traceforge/kicad/models3d.py +942 -0
- traceforge/kicad/outputs.py +81 -0
- traceforge/kicad/pcb.py +787 -0
- traceforge/kicad/planes.py +1242 -0
- traceforge/kicad/project.py +136 -0
- traceforge/kicad/route.py +2698 -0
- traceforge/kicad/routegeom.py +164 -0
- traceforge/kicad/schematic.py +268 -0
- traceforge/kicad/sides.py +34 -0
- traceforge/kicad/stackup.py +424 -0
- traceforge/kicad_plugin/__init__.py +12 -0
- traceforge/kicad_plugin/actions.py +92 -0
- traceforge/kicad_plugin/cli.py +66 -0
- traceforge/kicad_plugin/client.py +86 -0
- traceforge/kicad_plugin/context.py +257 -0
- traceforge/kicad_plugin/discovery.py +81 -0
- traceforge/kicad_plugin/install.py +203 -0
- traceforge/kicad_plugin/overlay.py +60 -0
- traceforge/kicad_plugin/payload/action_plugin.py +65 -0
- traceforge/kicad_plugin/payload/ipc_main.py +42 -0
- traceforge/kicad_plugin/payload/plugin.json +90 -0
- traceforge/kicad_plugin/payload/requirements.txt +1 -0
- traceforge/kicad_plugin/views.py +202 -0
- traceforge/langchain.py +57 -0
- traceforge/local.py +580 -0
- traceforge/mcp_server.py +304 -0
- traceforge/models.py +310 -0
- traceforge/next_steps.py +110 -0
- traceforge/parts/__init__.py +52 -0
- traceforge/parts/alternates.py +488 -0
- traceforge/parts/bom.py +423 -0
- traceforge/parts/brief.py +705 -0
- traceforge/parts/catalog.py +297 -0
- traceforge/parts/cli_sourcing.py +156 -0
- traceforge/parts/data/duty_rates.yaml +37 -0
- traceforge/parts/data/hs_codes.yaml +103 -0
- traceforge/parts/datasheet.py +278 -0
- traceforge/parts/fx.py +100 -0
- traceforge/parts/model.py +185 -0
- traceforge/parts/net.py +187 -0
- traceforge/parts/offers.py +46 -0
- traceforge/parts/packages.py +397 -0
- traceforge/parts/pinout.py +714 -0
- traceforge/parts/providers/__init__.py +17 -0
- traceforge/parts/providers/base.py +100 -0
- traceforge/parts/providers/jlc.py +364 -0
- traceforge/parts/providers/keyed.py +661 -0
- traceforge/parts/report.py +508 -0
- traceforge/parts/supply.py +330 -0
- traceforge/parts/verify.py +455 -0
- traceforge/parts/web.py +575 -0
- traceforge/plans.py +70 -0
- traceforge/preferences.py +127 -0
- traceforge/prices.py +130 -0
- traceforge/prices.yaml +28 -0
- traceforge/prompts/__init__.py +54 -0
- traceforge/prompts/system.md +103 -0
- traceforge/providers.py +764 -0
- traceforge/remote/__init__.py +33 -0
- traceforge/remote/client.py +422 -0
- traceforge/remote/oidc.py +240 -0
- traceforge/remote/orgs.py +510 -0
- traceforge/remote/server.py +690 -0
- traceforge/remote/store.py +100 -0
- traceforge/review_changes.py +187 -0
- traceforge/routing.py +99 -0
- traceforge/runtime.py +435 -0
- traceforge/sessions.py +324 -0
- traceforge/sexpr.py +279 -0
- traceforge/sim/__init__.py +17 -0
- traceforge/sim/acmath.py +234 -0
- traceforge/sim/circuit.py +499 -0
- traceforge/sim/cli.py +162 -0
- traceforge/sim/engine.py +172 -0
- traceforge/sim/ltspice.py +175 -0
- traceforge/sim/measure.py +123 -0
- traceforge/sim/models.py +76 -0
- traceforge/sim/project.py +218 -0
- traceforge/sim/rawfile.py +123 -0
- traceforge/sim/results.py +298 -0
- traceforge/sim/tune.py +172 -0
- traceforge/skill_commands.py +165 -0
- traceforge/skill_library/circuit-simulation/SKILL.md +30 -0
- traceforge/skill_library/compliance-review/SKILL.md +51 -0
- traceforge/skill_library/design-review/SKILL.md +47 -0
- traceforge/skill_library/errata-hunt/SKILL.md +30 -0
- traceforge/skill_library/firmware/SKILL.md +26 -0
- traceforge/skill_library/power-budget/SKILL.md +27 -0
- traceforge/skill_library/release-package/SKILL.md +29 -0
- traceforge/skill_library/sourcing-brief/SKILL.md +24 -0
- traceforge/skill_library/thermal-review/SKILL.md +15 -0
- traceforge/skills.py +131 -0
- traceforge/thermal.py +557 -0
- traceforge/tools/__init__.py +48 -0
- traceforge/tools/agent_tools.py +213 -0
- traceforge/tools/base.py +303 -0
- traceforge/tools/batch.py +155 -0
- traceforge/tools/connections.py +245 -0
- traceforge/tools/files.py +777 -0
- traceforge/tools/firmware_tools.py +218 -0
- traceforge/tools/kicad_tools.py +1072 -0
- traceforge/tools/models3d_tools.py +141 -0
- traceforge/tools/parts_tools.py +473 -0
- traceforge/tools/processes.py +246 -0
- traceforge/tools/review_tools.py +178 -0
- traceforge/tools/sim_tools.py +230 -0
- traceforge/tui.py +620 -0
- traceforge/updates.py +124 -0
- traceforge/validate/__init__.py +77 -0
- traceforge/validate/builtins.py +1091 -0
- traceforge/validate/circuits.py +1240 -0
- traceforge/validate/config.py +410 -0
- traceforge/validate/default.yaml +113 -0
- traceforge/validate/design.py +191 -0
- traceforge/validate/explain.py +112 -0
- traceforge/validate/grounding.py +169 -0
- traceforge/validate/ipc2221.py +350 -0
- traceforge/validate/multilayer.py +805 -0
- traceforge/validate/netmodel.py +261 -0
- traceforge/validate/partcheck.py +472 -0
- traceforge/validate/report.py +280 -0
- traceforge/validate/review.py +322 -0
- traceforge/validate/runner.py +662 -0
- traceforge/workspace.py +171 -0
- traceforge_pcb-1.0.0.dist-info/METADATA +463 -0
- traceforge_pcb-1.0.0.dist-info/RECORD +219 -0
- traceforge_pcb-1.0.0.dist-info/WHEEL +4 -0
- traceforge_pcb-1.0.0.dist-info/entry_points.txt +2 -0
- traceforge_pcb-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""Agent tools for 3D models: check, assign and render (see traceforge.kicad.models3d)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from traceforge.kicad import models3d
|
|
9
|
+
from traceforge.tools.base import Tool, ToolContext, ToolError, ToolKind, ToolOutput
|
|
10
|
+
from traceforge.tools.files import _image_output
|
|
11
|
+
from traceforge.tools.kicad_tools import _out_dir, _pcb_path
|
|
12
|
+
|
|
13
|
+
_PCB = {"type": "string", "description": "board path (default: the project's)"}
|
|
14
|
+
_VEC = {"type": "array", "items": {"type": "number"}, "minItems": 3, "maxItems": 3}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _model_file(raw: str, ctx: ToolContext) -> Path:
|
|
18
|
+
"""A model inside the workspace, or one inside an installed KiCad 3D library."""
|
|
19
|
+
candidate = Path(raw).expanduser()
|
|
20
|
+
if candidate.is_absolute() and candidate.is_file():
|
|
21
|
+
resolved = candidate.resolve()
|
|
22
|
+
for base in models3d.model_dirs():
|
|
23
|
+
if base.resolve() in resolved.parents:
|
|
24
|
+
return resolved
|
|
25
|
+
return ctx.resolve(raw, must_exist=True)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def models_check(args: dict[str, Any], ctx: ToolContext) -> dict:
|
|
29
|
+
pcb = _pcb_path(args, ctx)
|
|
30
|
+
rows = models3d.inventory(pcb)
|
|
31
|
+
problems = [r.to_dict() for r in rows if r.status != "resolved"]
|
|
32
|
+
findings = models3d.check_models(pcb)
|
|
33
|
+
return {
|
|
34
|
+
"summary": models3d.summary(rows),
|
|
35
|
+
"model_libraries": [str(d) for d in models3d.model_dirs()],
|
|
36
|
+
"problems": problems,
|
|
37
|
+
"findings": [f.to_dict() for f in findings],
|
|
38
|
+
"note": "proposals carry a confidence; only 0.90 and above are exact name matches",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def models_assign(args: dict[str, Any], ctx: ToolContext) -> dict:
|
|
43
|
+
pcb = _pcb_path(args, ctx, for_write=True)
|
|
44
|
+
overwrite = bool(args.get("overwrite"))
|
|
45
|
+
try:
|
|
46
|
+
if args.get("auto"):
|
|
47
|
+
return {
|
|
48
|
+
"results": models3d.assign_proposed(pcb, float(args.get("min_confidence") or 0.9))
|
|
49
|
+
}
|
|
50
|
+
ref = args.get("ref")
|
|
51
|
+
if not ref:
|
|
52
|
+
raise ToolError("pass 'ref' (with 'model' or 'generate') or auto=true")
|
|
53
|
+
if args.get("generate"):
|
|
54
|
+
return models3d.assign_generic(pcb, ref, args["generate"], overwrite=overwrite)
|
|
55
|
+
if not args.get("model"):
|
|
56
|
+
raise ToolError("pass 'model' (a .step/.wrl file), 'generate', or auto=true")
|
|
57
|
+
vec = lambda key, default: tuple(float(v) for v in args.get(key) or default) # noqa: E731
|
|
58
|
+
return models3d.assign_model(
|
|
59
|
+
pcb,
|
|
60
|
+
ref,
|
|
61
|
+
_model_file(args["model"], ctx),
|
|
62
|
+
vec("offset", (0, 0, 0)),
|
|
63
|
+
vec("scale", (1, 1, 1)),
|
|
64
|
+
vec("rotate", (0, 0, 0)),
|
|
65
|
+
overwrite=overwrite,
|
|
66
|
+
)
|
|
67
|
+
except models3d.ModelError as exc:
|
|
68
|
+
raise ToolError(str(exc)) from exc
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def render_3d(args: dict[str, Any], ctx: ToolContext) -> ToolOutput:
|
|
72
|
+
pcb = _pcb_path(args, ctx)
|
|
73
|
+
side = args.get("side") or "top"
|
|
74
|
+
out = _out_dir(ctx, "render") / f"{pcb.stem}-{side}.png"
|
|
75
|
+
try:
|
|
76
|
+
models3d.render_3d(
|
|
77
|
+
pcb,
|
|
78
|
+
out,
|
|
79
|
+
side,
|
|
80
|
+
width=int(args.get("width") or 1600),
|
|
81
|
+
height=int(args.get("height") or 900),
|
|
82
|
+
quality=args.get("quality") or "basic",
|
|
83
|
+
perspective=bool(args.get("perspective")),
|
|
84
|
+
)
|
|
85
|
+
except models3d.RenderError as exc:
|
|
86
|
+
raise ToolError(str(exc)) from exc
|
|
87
|
+
return _image_output(out, ctx)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
MODELS3D_TOOLS = [
|
|
91
|
+
Tool(
|
|
92
|
+
"kicad_models_check",
|
|
93
|
+
"Check every footprint's 3D model: resolved path, missing file, or no model. Lists "
|
|
94
|
+
"library proposals with a confidence for footprints that need one. Read-only.",
|
|
95
|
+
{"type": "object", "properties": {"pcb": _PCB}},
|
|
96
|
+
models_check,
|
|
97
|
+
),
|
|
98
|
+
Tool(
|
|
99
|
+
"kicad_models_assign",
|
|
100
|
+
"Write a (model ...) entry into a board footprint. Give ref + model (a .step/.wrl in the "
|
|
101
|
+
"workspace or KiCad's 3D library; files outside the project are copied to 3dmodels/), or "
|
|
102
|
+
"ref + generate ('step'|'wrl') for a labelled generic placeholder sized from the "
|
|
103
|
+
"footprint, or auto=true to assign exact library matches (min_confidence, default 0.9). "
|
|
104
|
+
"Never replaces an existing model unless overwrite=true.",
|
|
105
|
+
{
|
|
106
|
+
"type": "object",
|
|
107
|
+
"properties": {
|
|
108
|
+
"pcb": _PCB,
|
|
109
|
+
"ref": {"type": "string"},
|
|
110
|
+
"model": {"type": "string"},
|
|
111
|
+
"generate": {"type": "string", "enum": ["step", "wrl"]},
|
|
112
|
+
"auto": {"type": "boolean"},
|
|
113
|
+
"min_confidence": {"type": "number"},
|
|
114
|
+
"offset": _VEC,
|
|
115
|
+
"scale": _VEC,
|
|
116
|
+
"rotate": _VEC,
|
|
117
|
+
"overwrite": {"type": "boolean"},
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
models_assign,
|
|
121
|
+
ToolKind.WRITE,
|
|
122
|
+
"pcb",
|
|
123
|
+
),
|
|
124
|
+
Tool(
|
|
125
|
+
"kicad_render_3d",
|
|
126
|
+
"Render the board in 3D (top or bottom) with `kicad-cli pcb render` and show the image. "
|
|
127
|
+
"Needs KiCad 8 or later; on KiCad 7 it reports that plainly.",
|
|
128
|
+
{
|
|
129
|
+
"type": "object",
|
|
130
|
+
"properties": {
|
|
131
|
+
"pcb": _PCB,
|
|
132
|
+
"side": {"type": "string", "enum": ["top", "bottom"]},
|
|
133
|
+
"width": {"type": "integer"},
|
|
134
|
+
"height": {"type": "integer"},
|
|
135
|
+
"quality": {"type": "string", "enum": ["basic", "high"]},
|
|
136
|
+
"perspective": {"type": "boolean"},
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
render_3d,
|
|
140
|
+
),
|
|
141
|
+
]
|
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
"""Agent tools for live part data, datasheets and the web. All read-only."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from traceforge.parts import Offline, ProviderError, default_catalog, is_lcsc_code
|
|
10
|
+
from traceforge.tools.base import Tool, ToolContext, ToolError, ToolKind
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _offline_error(exc: Exception) -> ToolError:
|
|
14
|
+
return ToolError(
|
|
15
|
+
f"not verified: offline or unreachable ({exc}). Do not guess part numbers; say they are "
|
|
16
|
+
"unverified."
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def part_lookup(args: dict[str, Any], ctx: ToolContext) -> dict:
|
|
21
|
+
catalog = default_catalog()
|
|
22
|
+
queries = [q for q in [args.get("part"), *(args.get("parts") or [])] if q and str(q).strip()]
|
|
23
|
+
if not queries:
|
|
24
|
+
raise ToolError("give 'part' (an LCSC code such as C2286 or an MPN) or 'parts'")
|
|
25
|
+
reports = catalog.lookup_many([str(q) for q in queries[:40]])
|
|
26
|
+
out: dict[str, Any] = {}
|
|
27
|
+
for query, report in reports.items():
|
|
28
|
+
if report.part is not None:
|
|
29
|
+
out[query] = report.part.to_dict(brief=not args.get("full"))
|
|
30
|
+
elif report.answered:
|
|
31
|
+
out[query] = {
|
|
32
|
+
"found": False,
|
|
33
|
+
"note": "no catalogue knows this part number"
|
|
34
|
+
+ ("; it is not a valid LCSC code" if not is_lcsc_code(query) else ""),
|
|
35
|
+
"asked": report.describe(),
|
|
36
|
+
}
|
|
37
|
+
else:
|
|
38
|
+
out[query] = {
|
|
39
|
+
"found": False,
|
|
40
|
+
"note": "not verified: offline",
|
|
41
|
+
"asked": report.describe(),
|
|
42
|
+
}
|
|
43
|
+
return out
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def part_search(args: dict[str, Any], ctx: ToolContext) -> dict:
|
|
47
|
+
try:
|
|
48
|
+
results = default_catalog().search(
|
|
49
|
+
args["query"],
|
|
50
|
+
package=args.get("package"),
|
|
51
|
+
basic_only=bool(args.get("basic_only", False)),
|
|
52
|
+
in_stock=bool(args.get("in_stock", True)),
|
|
53
|
+
limit=int(args.get("limit") or 10),
|
|
54
|
+
)
|
|
55
|
+
except Offline as exc:
|
|
56
|
+
raise _offline_error(exc) from exc
|
|
57
|
+
return {
|
|
58
|
+
"query": args["query"],
|
|
59
|
+
"results": [p.to_dict(brief=True) for p in results],
|
|
60
|
+
"note": "sorted basic first, then by stock. Verify with part_lookup before writing an "
|
|
61
|
+
"LCSC/MPN field, and check the package against the footprint."
|
|
62
|
+
if results
|
|
63
|
+
else "no matches: use fewer words (MPN prefix, value + package) or drop filters",
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def parts_check(args: dict[str, Any], ctx: ToolContext) -> dict:
|
|
68
|
+
from traceforge.checks import summarize
|
|
69
|
+
from traceforge.kicad.pcb import read_board
|
|
70
|
+
from traceforge.kicad.project import discover
|
|
71
|
+
from traceforge.kicad.schematic import read_schematic
|
|
72
|
+
from traceforge.parts.verify import check_parts
|
|
73
|
+
|
|
74
|
+
ws = discover(ctx.workspace)
|
|
75
|
+
schematic = (
|
|
76
|
+
ctx.resolve(args["schematic"], must_exist=True) if args.get("schematic") else ws.schematic
|
|
77
|
+
)
|
|
78
|
+
if schematic is None:
|
|
79
|
+
raise ToolError("no schematic found")
|
|
80
|
+
pcb = ctx.resolve(args["pcb"], must_exist=True) if args.get("pcb") else ws.pcb
|
|
81
|
+
board = read_board(pcb) if pcb else None
|
|
82
|
+
findings, summary = check_parts(read_schematic(schematic), board)
|
|
83
|
+
return {"summary": summary, **summarize(findings)}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def parts_verify(args: dict[str, Any], ctx: ToolContext) -> dict:
|
|
87
|
+
from traceforge.parts.report import brief, verify_design
|
|
88
|
+
|
|
89
|
+
folder = ctx.resolve(args["path"], must_exist=True) if args.get("path") else ctx.workspace
|
|
90
|
+
data = verify_design(folder)
|
|
91
|
+
ctx.state["parts_verified"] = data["all_verified"]
|
|
92
|
+
out = brief(data)
|
|
93
|
+
if out.get("report"):
|
|
94
|
+
out["report"] = ctx.display(Path(out["report"]))
|
|
95
|
+
return out
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def datasheet_fetch(args: dict[str, Any], ctx: ToolContext) -> dict:
|
|
99
|
+
from traceforge.parts.datasheet import fetch_datasheet, select
|
|
100
|
+
|
|
101
|
+
folder = ctx.workspace / ".traceforge" / "datasheets"
|
|
102
|
+
url = args.get("url")
|
|
103
|
+
name = None
|
|
104
|
+
local: Path | None = None
|
|
105
|
+
part_info = None
|
|
106
|
+
if args.get("path"):
|
|
107
|
+
local = ctx.resolve(args["path"], must_exist=True)
|
|
108
|
+
elif not url:
|
|
109
|
+
part = args.get("part")
|
|
110
|
+
if not part:
|
|
111
|
+
raise ToolError("give 'url', 'part' (LCSC code or MPN) or 'path'")
|
|
112
|
+
try:
|
|
113
|
+
info = default_catalog().lookup(part)
|
|
114
|
+
except Offline as exc:
|
|
115
|
+
raise _offline_error(exc) from exc
|
|
116
|
+
if info is None or not info.datasheet_url:
|
|
117
|
+
raise ToolError(f"no datasheet URL known for {part}; try web_search")
|
|
118
|
+
url, name = info.datasheet_url, f"{info.lcsc or ''}_{info.mpn}"
|
|
119
|
+
part_info = {"lcsc": info.lcsc, "mpn": info.mpn, "manufacturer": info.manufacturer}
|
|
120
|
+
try:
|
|
121
|
+
path, pages, outline = fetch_datasheet(folder, url=url, name=name, local=local)
|
|
122
|
+
except Offline as exc:
|
|
123
|
+
raise _offline_error(exc) from exc
|
|
124
|
+
except ProviderError as exc:
|
|
125
|
+
raise ToolError(str(exc)) from exc
|
|
126
|
+
result = select(
|
|
127
|
+
pages,
|
|
128
|
+
outline,
|
|
129
|
+
page_spec=args.get("pages"),
|
|
130
|
+
query=args.get("query"),
|
|
131
|
+
max_chars=int(args.get("max_chars") or 24_000),
|
|
132
|
+
)
|
|
133
|
+
result = {"saved": ctx.display(path), "source": url or str(local), **result}
|
|
134
|
+
if part_info:
|
|
135
|
+
result["part"] = part_info
|
|
136
|
+
return result
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def web_search_tool(args: dict[str, Any], ctx: ToolContext) -> dict:
|
|
140
|
+
from traceforge.parts.web import web_search
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
answer = web_search(args["query"], limit=int(args.get("limit") or 8))
|
|
144
|
+
except Offline as exc:
|
|
145
|
+
raise _offline_error(exc) from exc
|
|
146
|
+
except ProviderError as exc:
|
|
147
|
+
raise ToolError(str(exc)) from exc
|
|
148
|
+
return answer.to_dict()
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def web_fetch_tool(args: dict[str, Any], ctx: ToolContext) -> dict:
|
|
152
|
+
from traceforge.parts.web import web_fetch
|
|
153
|
+
|
|
154
|
+
try:
|
|
155
|
+
return web_fetch(
|
|
156
|
+
args["url"],
|
|
157
|
+
max_chars=min(int(args.get("max_chars") or 20_000), 60_000),
|
|
158
|
+
offset=int(args.get("offset") or 0),
|
|
159
|
+
find=args.get("find"),
|
|
160
|
+
)
|
|
161
|
+
except Offline as exc:
|
|
162
|
+
raise _offline_error(exc) from exc
|
|
163
|
+
except ProviderError as exc:
|
|
164
|
+
raise ToolError(str(exc)) from exc
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def reference_projects(args: dict[str, Any], ctx: ToolContext) -> dict:
|
|
168
|
+
from traceforge.parts.web import reference_project_search
|
|
169
|
+
|
|
170
|
+
return reference_project_search(
|
|
171
|
+
args["query"], tool=args.get("tool") or "kicad", limit=int(args.get("limit") or 8)
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _qty_list(args: dict[str, Any], key: str, default: tuple[int, ...]) -> tuple[int, ...]:
|
|
176
|
+
raw = args.get(key)
|
|
177
|
+
if not raw:
|
|
178
|
+
return default
|
|
179
|
+
try:
|
|
180
|
+
values = sorted({int(x) for x in raw})
|
|
181
|
+
except (TypeError, ValueError) as exc:
|
|
182
|
+
raise ToolError(f"{key} must be a list of whole numbers") from exc
|
|
183
|
+
if not values or values[0] < 1:
|
|
184
|
+
raise ToolError(f"{key} must be positive")
|
|
185
|
+
return tuple(values)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def sourcing_compare(args: dict[str, Any], ctx: ToolContext) -> dict:
|
|
189
|
+
"""Merged supplier comparison for one part."""
|
|
190
|
+
from traceforge.parts.supply import source_part
|
|
191
|
+
|
|
192
|
+
if not args.get("part"):
|
|
193
|
+
raise ToolError("give 'part' (LCSC code or MPN)")
|
|
194
|
+
result = source_part(
|
|
195
|
+
str(args["part"]),
|
|
196
|
+
qtys=_qty_list(args, "quantities", (1, 10, 100, 1000)),
|
|
197
|
+
currency=str(args.get("currency") or "USD"),
|
|
198
|
+
destination=str(args.get("country") or ""),
|
|
199
|
+
)
|
|
200
|
+
return result.to_dict()
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def parts_alternates(args: dict[str, Any], ctx: ToolContext) -> dict:
|
|
204
|
+
"""Alternates by footprint and electrical match. Suggestions only."""
|
|
205
|
+
from traceforge.parts.alternates import find_alternates
|
|
206
|
+
|
|
207
|
+
if not args.get("part"):
|
|
208
|
+
raise ToolError("give 'part' (LCSC code or MPN)")
|
|
209
|
+
try:
|
|
210
|
+
return find_alternates(str(args["part"]), limit=int(args.get("limit") or 5)).to_dict()
|
|
211
|
+
except Offline as exc:
|
|
212
|
+
raise _offline_error(exc) from exc
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def sourcing_brief(args: dict[str, Any], ctx: ToolContext) -> dict:
|
|
216
|
+
"""Cited sourcing brief for one part (HS code, distributors, risk, price bands, landed cost)."""
|
|
217
|
+
from traceforge.parts.brief import build_brief
|
|
218
|
+
|
|
219
|
+
if not args.get("part"):
|
|
220
|
+
raise ToolError("give 'part' (LCSC code or MPN)")
|
|
221
|
+
try:
|
|
222
|
+
brief = build_brief(
|
|
223
|
+
str(args["part"]),
|
|
224
|
+
country=str(args.get("country") or ""),
|
|
225
|
+
currency=str(args.get("currency") or "USD"),
|
|
226
|
+
qty=int(args.get("quantity") or 1000),
|
|
227
|
+
)
|
|
228
|
+
except Offline as exc:
|
|
229
|
+
raise _offline_error(exc) from exc
|
|
230
|
+
data = brief.to_dict()
|
|
231
|
+
if args.get("write"):
|
|
232
|
+
base = ctx.resolve(str(args["write"]), for_write=True)
|
|
233
|
+
base.parent.mkdir(parents=True, exist_ok=True)
|
|
234
|
+
base.with_suffix(".md").write_text(brief.to_markdown() + "\n", encoding="utf-8")
|
|
235
|
+
base.with_suffix(".json").write_text(
|
|
236
|
+
json.dumps(data, indent=1, default=str), encoding="utf-8"
|
|
237
|
+
)
|
|
238
|
+
data["written"] = [str(base.with_suffix(".md")), str(base.with_suffix(".json"))]
|
|
239
|
+
else:
|
|
240
|
+
data["markdown"] = brief.to_markdown()
|
|
241
|
+
return data
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def bom_cost(args: dict[str, Any], ctx: ToolContext) -> dict:
|
|
245
|
+
"""Priced BOM roll-up (cheapest eligible supplier per line) and optional CSV exports."""
|
|
246
|
+
from traceforge.parts.bom import export_bom, load_bom, rollup
|
|
247
|
+
from traceforge.parts.fx import load_rates
|
|
248
|
+
from traceforge.parts.supply import source_many
|
|
249
|
+
|
|
250
|
+
path = ctx.resolve(str(args.get("path") or "."), must_exist=True)
|
|
251
|
+
try:
|
|
252
|
+
items = load_bom(path)
|
|
253
|
+
except (ValueError, OSError) as exc:
|
|
254
|
+
raise ToolError(str(exc)) from exc
|
|
255
|
+
boards = _qty_list(args, "board_quantities", (1, 10, 100, 1000))
|
|
256
|
+
currency = str(args.get("currency") or "USD")
|
|
257
|
+
fx = load_rates()
|
|
258
|
+
try:
|
|
259
|
+
sourcings = source_many(
|
|
260
|
+
[i.query for i in items if i.query],
|
|
261
|
+
qtys=tuple(sorted({q * i.qty for q in boards for i in items})),
|
|
262
|
+
currency=currency,
|
|
263
|
+
fx=fx,
|
|
264
|
+
)
|
|
265
|
+
except Offline as exc:
|
|
266
|
+
raise _offline_error(exc) from exc
|
|
267
|
+
roll = rollup(items, sourcings, boards, currency.upper())
|
|
268
|
+
out = roll.to_dict()
|
|
269
|
+
out["fx"] = fx.describe()
|
|
270
|
+
out["markdown"] = roll.to_markdown()
|
|
271
|
+
if args.get("export_dir"):
|
|
272
|
+
folder = ctx.resolve(str(args["export_dir"]), for_write=True)
|
|
273
|
+
folder.mkdir(parents=True, exist_ok=True)
|
|
274
|
+
written = []
|
|
275
|
+
for fmt in ("jlcpcb", "pcbway", "generic", "excel"):
|
|
276
|
+
target = folder / f"bom_{fmt}.csv"
|
|
277
|
+
target.write_text(
|
|
278
|
+
export_bom(items, fmt, sourcings, boards[0]), encoding="utf-8", newline=""
|
|
279
|
+
)
|
|
280
|
+
written.append(str(target))
|
|
281
|
+
out["written"] = written
|
|
282
|
+
return out
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
PART_TOOLS = [
|
|
286
|
+
Tool(
|
|
287
|
+
"part_lookup",
|
|
288
|
+
"Look up parts live in the JLCPCB/LCSC catalogues (and Octopart/DigiKey/Mouser when "
|
|
289
|
+
"configured): manufacturer, MPN, package, pin count, stock, price, JLCPCB basic/"
|
|
290
|
+
"extended, datasheet URL, key attributes. Give an LCSC code (C2286) or MPN; 'parts' "
|
|
291
|
+
"checks many at once. Use it for every part number you write or review.",
|
|
292
|
+
{
|
|
293
|
+
"type": "object",
|
|
294
|
+
"properties": {
|
|
295
|
+
"part": {"type": "string"},
|
|
296
|
+
"parts": {"type": "array", "items": {"type": "string"}},
|
|
297
|
+
"full": {"type": "boolean", "description": "all attributes and price breaks"},
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
part_lookup,
|
|
301
|
+
),
|
|
302
|
+
Tool(
|
|
303
|
+
"part_search",
|
|
304
|
+
"Search orderable parts by MPN prefix, value and package or keywords ('10k 0603 1%', "
|
|
305
|
+
"'100nF 0402', 'AMS1117-3.3', 'usb type-c 16p'). Returns JLCPCB basic parts first, then "
|
|
306
|
+
"by stock. Pick part numbers from these results, never from memory.",
|
|
307
|
+
{
|
|
308
|
+
"type": "object",
|
|
309
|
+
"properties": {
|
|
310
|
+
"query": {"type": "string"},
|
|
311
|
+
"package": {"type": "string", "description": "e.g. 0603, SOT-23-5, SOIC-8"},
|
|
312
|
+
"basic_only": {"type": "boolean"},
|
|
313
|
+
"in_stock": {"type": "boolean", "description": "default true"},
|
|
314
|
+
"limit": {"type": "integer"},
|
|
315
|
+
},
|
|
316
|
+
"required": ["query"],
|
|
317
|
+
},
|
|
318
|
+
part_search,
|
|
319
|
+
),
|
|
320
|
+
Tool(
|
|
321
|
+
"parts_check",
|
|
322
|
+
"Verify every LCSC/MPN in the project against the live catalogue: part exists, package "
|
|
323
|
+
"fits the footprint (size, SOIC width, pin count, pitch, SMD/THT), value and MPN match, "
|
|
324
|
+
"stock, lifecycle, basic vs extended. Returns P0-P3 findings with evidence.",
|
|
325
|
+
{
|
|
326
|
+
"type": "object",
|
|
327
|
+
"properties": {"schematic": {"type": "string"}, "pcb": {"type": "string"}},
|
|
328
|
+
},
|
|
329
|
+
parts_check,
|
|
330
|
+
),
|
|
331
|
+
Tool(
|
|
332
|
+
"parts_verify",
|
|
333
|
+
"Verify every BOM line (part exists, package vs footprint, pin count, datasheet pinout "
|
|
334
|
+
"vs symbol pins and footprint pads, stock, basic/extended) and write "
|
|
335
|
+
".traceforge/reports/parts-verification.md/.json. The design is not finished until "
|
|
336
|
+
"this reports 'all parts verified'.",
|
|
337
|
+
{
|
|
338
|
+
"type": "object",
|
|
339
|
+
"properties": {"path": {"type": "string", "description": "project folder"}},
|
|
340
|
+
},
|
|
341
|
+
parts_verify,
|
|
342
|
+
),
|
|
343
|
+
Tool(
|
|
344
|
+
"datasheet_fetch",
|
|
345
|
+
"Download a datasheet (by url, by part via its catalogue datasheet, or a local PDF path) "
|
|
346
|
+
"to .traceforge/datasheets/ and return text: the pages you ask for ('1-3,12'), the pages "
|
|
347
|
+
"that best match 'query' (e.g. 'package dimensions 8S2', 'absolute maximum', 'typical "
|
|
348
|
+
"application'), or by default the first pages and the outline.",
|
|
349
|
+
{
|
|
350
|
+
"type": "object",
|
|
351
|
+
"properties": {
|
|
352
|
+
"url": {"type": "string"},
|
|
353
|
+
"part": {"type": "string", "description": "LCSC code or MPN"},
|
|
354
|
+
"path": {"type": "string", "description": "a PDF already in the workspace"},
|
|
355
|
+
"pages": {"type": "string"},
|
|
356
|
+
"query": {"type": "string"},
|
|
357
|
+
"max_chars": {"type": "integer"},
|
|
358
|
+
},
|
|
359
|
+
},
|
|
360
|
+
datasheet_fetch,
|
|
361
|
+
),
|
|
362
|
+
Tool(
|
|
363
|
+
"sourcing_compare",
|
|
364
|
+
"Merged supplier comparison for one part across every configured source (JLCPCB, LCSC, "
|
|
365
|
+
"jlcsearch, plus DigiKey/Mouser/Nexar when keyed): price breaks, stock, MOQ, lead time, "
|
|
366
|
+
"lifecycle, best price per quantity and a recommended supplier, in one currency with the "
|
|
367
|
+
"exchange-rate table named.",
|
|
368
|
+
{
|
|
369
|
+
"type": "object",
|
|
370
|
+
"properties": {
|
|
371
|
+
"part": {"type": "string", "description": "LCSC code or MPN"},
|
|
372
|
+
"quantities": {"type": "array", "items": {"type": "integer"}},
|
|
373
|
+
"currency": {"type": "string"},
|
|
374
|
+
"country": {"type": "string"},
|
|
375
|
+
},
|
|
376
|
+
"required": ["part"],
|
|
377
|
+
},
|
|
378
|
+
sourcing_compare,
|
|
379
|
+
),
|
|
380
|
+
Tool(
|
|
381
|
+
"parts_alternates",
|
|
382
|
+
"Alternates by footprint and electrical match (passives: value, tolerance, rating, "
|
|
383
|
+
"package, dielectric; MCUs: pin-compatible family suggestions with confidence). Each "
|
|
384
|
+
"carries reasons and differences. Suggestions only: never substitute without telling the user.",
|
|
385
|
+
{
|
|
386
|
+
"type": "object",
|
|
387
|
+
"properties": {"part": {"type": "string"}, "limit": {"type": "integer"}},
|
|
388
|
+
"required": ["part"],
|
|
389
|
+
},
|
|
390
|
+
parts_alternates,
|
|
391
|
+
),
|
|
392
|
+
Tool(
|
|
393
|
+
"sourcing_brief",
|
|
394
|
+
"Cited sourcing brief for one part: suppliers, HS code with rationale (a guide to confirm "
|
|
395
|
+
"with the customs broker), authorised distributors, single-source risk, volume price bands "
|
|
396
|
+
"and a landed-cost estimate with the arithmetic. Pass 'write' (path without extension) to "
|
|
397
|
+
"save Markdown and JSON.",
|
|
398
|
+
{
|
|
399
|
+
"type": "object",
|
|
400
|
+
"properties": {
|
|
401
|
+
"part": {"type": "string"},
|
|
402
|
+
"country": {"type": "string", "description": "destination, e.g. IN"},
|
|
403
|
+
"quantity": {"type": "integer"},
|
|
404
|
+
"currency": {"type": "string"},
|
|
405
|
+
"write": {"type": "string"},
|
|
406
|
+
},
|
|
407
|
+
"required": ["part"],
|
|
408
|
+
},
|
|
409
|
+
sourcing_brief,
|
|
410
|
+
ToolKind.WRITE,
|
|
411
|
+
"write",
|
|
412
|
+
),
|
|
413
|
+
Tool(
|
|
414
|
+
"bom_cost",
|
|
415
|
+
"Price a whole BOM (design.yaml, KiCad schematic, project folder or BOM CSV): cheapest "
|
|
416
|
+
"eligible supplier per line and totals at 1/10/100/1000 boards. 'export_dir' also writes "
|
|
417
|
+
"JLCPCB, PCBWay, generic and Excel CSV BOMs.",
|
|
418
|
+
{
|
|
419
|
+
"type": "object",
|
|
420
|
+
"properties": {
|
|
421
|
+
"path": {"type": "string"},
|
|
422
|
+
"board_quantities": {"type": "array", "items": {"type": "integer"}},
|
|
423
|
+
"currency": {"type": "string"},
|
|
424
|
+
"export_dir": {"type": "string"},
|
|
425
|
+
},
|
|
426
|
+
},
|
|
427
|
+
bom_cost,
|
|
428
|
+
ToolKind.WRITE,
|
|
429
|
+
"export_dir",
|
|
430
|
+
),
|
|
431
|
+
Tool(
|
|
432
|
+
"web_search",
|
|
433
|
+
"Search the web (read-only): app notes, errata, reference values, part comparisons. "
|
|
434
|
+
"Returns titles, URLs, snippets and, for some providers, a cited answer.",
|
|
435
|
+
{
|
|
436
|
+
"type": "object",
|
|
437
|
+
"properties": {"query": {"type": "string"}, "limit": {"type": "integer"}},
|
|
438
|
+
"required": ["query"],
|
|
439
|
+
},
|
|
440
|
+
web_search_tool,
|
|
441
|
+
),
|
|
442
|
+
Tool(
|
|
443
|
+
"web_fetch",
|
|
444
|
+
"Fetch a public web page or PDF as plain text (read-only). Use 'find' to return only "
|
|
445
|
+
"the passages around a phrase, or offset/max_chars to page through long documents.",
|
|
446
|
+
{
|
|
447
|
+
"type": "object",
|
|
448
|
+
"properties": {
|
|
449
|
+
"url": {"type": "string"},
|
|
450
|
+
"find": {"type": "string"},
|
|
451
|
+
"offset": {"type": "integer"},
|
|
452
|
+
"max_chars": {"type": "integer"},
|
|
453
|
+
},
|
|
454
|
+
"required": ["url"],
|
|
455
|
+
},
|
|
456
|
+
web_fetch_tool,
|
|
457
|
+
),
|
|
458
|
+
Tool(
|
|
459
|
+
"reference_project_search",
|
|
460
|
+
"Find open-source reference designs (GitHub, OSHWLab, Hackaday.io) that use a part or "
|
|
461
|
+
"implement a circuit block, to compare against proven schematics and values.",
|
|
462
|
+
{
|
|
463
|
+
"type": "object",
|
|
464
|
+
"properties": {
|
|
465
|
+
"query": {"type": "string", "description": "e.g. 'ATtiny85 USB-C', 'CH340C'"},
|
|
466
|
+
"tool": {"type": "string", "enum": ["kicad", "altium", "any"]},
|
|
467
|
+
"limit": {"type": "integer"},
|
|
468
|
+
},
|
|
469
|
+
"required": ["query"],
|
|
470
|
+
},
|
|
471
|
+
reference_projects,
|
|
472
|
+
),
|
|
473
|
+
]
|