Split out of the agent repository into its own project. The two commits that introduced and reworked the extension are preserved. The extension could not build on its own: prebuild called `python ../../tools/generate_tool_defs.py`, and the 31 tool schemas lived outside the tree — in a directory that was under no version control at all. - tools/schema/*.json and tools/generate_tool_defs.py now live here. They describe browser tools, so they belong with the extension. - The generator writes TypeScript into this repo and, when the agent repo is checked out next to it, the Python tool definitions there as well. The presence check looks for agent/tools.py: checking only for the directory was useless — it is almost always true, and the generator then wrote into an arbitrary neighbouring folder (verified, then fixed). - Added .gitignore: dist/, node_modules/, and *.token — the MCP bridge token is a runtime secret. Verified: `npm run build` from a clean checkout with and without the agent repo present; tsc reports no errors. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
129 lines
4.6 KiB
Python
129 lines
4.6 KiB
Python
"""Generator: tools/schema/*.json → Python ToolSpecs + TypeScript types.
|
|
|
|
Liest alle JSON-Dateien aus tools/schema/, erzeugt:
|
|
1. agent/browser_tool_defs.py — Liste von Bedrock-toolSpec-Dicts für den Broker
|
|
2. extension/src/shared/tool-schemas.generated.ts — TypeScript-Typen + Registry
|
|
|
|
Aufruf:
|
|
python tools/generate_tool_defs.py
|
|
|
|
Im esbuild-Step der Extension als Pre-Build verdrahtet (package.json "prebuild").
|
|
"""
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
|
|
SCHEMA_DIR = Path(__file__).parent / "schema"
|
|
|
|
#: Wurzel dieses Repos (tools/ liegt direkt darunter).
|
|
_ROOT = Path(__file__).parent.parent
|
|
|
|
#: TypeScript-Ziel — immer vorhanden, dies ist das eigene Repo.
|
|
TS_OUTPUT = _ROOT / "src" / "shared" / "tool-schemas.generated.ts"
|
|
|
|
#: Python-Ziel im NACHBAR-Repo (agent/). Die Schemata beschreiben Browser-Tools
|
|
#: und gehoeren damit zur Extension; der Python-Broker konsumiert nur das
|
|
#: Erzeugnis, das dort eingecheckt ist. Liegen die Repos nicht nebeneinander,
|
|
#: wird der Python-Teil uebersprungen statt abzubrechen — die Extension muss
|
|
#: allein baubar bleiben.
|
|
PY_OUTPUT = _ROOT.parent / "browser_tool_defs.py"
|
|
|
|
|
|
def load_schemas() -> list[dict]:
|
|
schemas = []
|
|
for f in sorted(SCHEMA_DIR.glob("*.json")):
|
|
with open(f, encoding="utf-8") as fh:
|
|
schemas.append(json.load(fh))
|
|
return schemas
|
|
|
|
|
|
def generate_python(schemas: list[dict]) -> str:
|
|
lines = [
|
|
'"""Auto-generated browser tool definitions for the Bedrock Converse API.',
|
|
'',
|
|
'DO NOT EDIT — generated by tools/generate_tool_defs.py from tools/schema/*.json.',
|
|
'"""',
|
|
'',
|
|
'BROWSER_TOOL_SPECS: list[dict] = [',
|
|
]
|
|
for s in schemas:
|
|
spec = {
|
|
"toolSpec": {
|
|
"name": s["name"],
|
|
"description": s["description"],
|
|
"inputSchema": {"json": s["inputSchema"]},
|
|
}
|
|
}
|
|
lines.append(f" {json.dumps(spec, ensure_ascii=False)},")
|
|
lines.append("]")
|
|
lines.append("")
|
|
lines.append("# Mapping: tool name -> requires_confirmation flag")
|
|
lines.append("BROWSER_TOOL_CONFIRMATION: dict[str, bool] = {")
|
|
for s in schemas:
|
|
lines.append(f' "{s["name"]}": {str(s.get("requires_confirmation", False))},')
|
|
lines.append("}")
|
|
lines.append("")
|
|
lines.append(f"BROWSER_TOOL_NAMES: list[str] = {json.dumps([s['name'] for s in schemas])}")
|
|
lines.append("")
|
|
return "\n".join(lines)
|
|
|
|
|
|
def generate_typescript(schemas: list[dict]) -> str:
|
|
lines = [
|
|
"/**",
|
|
" * Auto-generated browser tool schemas and types.",
|
|
" * DO NOT EDIT — generated by tools/generate_tool_defs.py from tools/schema/*.json.",
|
|
" */",
|
|
"",
|
|
"export interface BrowserToolSchema {",
|
|
" name: string;",
|
|
" description: string;",
|
|
" requires_confirmation: boolean;",
|
|
" inputSchema: Record<string, unknown>;",
|
|
"}",
|
|
"",
|
|
"export const BROWSER_TOOL_SCHEMAS: BrowserToolSchema[] = [",
|
|
]
|
|
for s in schemas:
|
|
lines.append(f" {json.dumps(s, ensure_ascii=False)},")
|
|
lines.append("];")
|
|
lines.append("")
|
|
lines.append("export const BROWSER_TOOL_NAMES = [")
|
|
for s in schemas:
|
|
lines.append(f' "{s["name"]}",')
|
|
lines.append("] as const;")
|
|
lines.append("")
|
|
lines.append("export type BrowserToolName = typeof BROWSER_TOOL_NAMES[number];")
|
|
lines.append("")
|
|
lines.append("export const REQUIRES_CONFIRMATION: Record<string, boolean> = {")
|
|
for s in schemas:
|
|
lines.append(f' "{s["name"]}": {str(s.get("requires_confirmation", False)).lower()},')
|
|
lines.append("};")
|
|
lines.append("")
|
|
return "\n".join(lines)
|
|
|
|
|
|
def main():
|
|
schemas = load_schemas()
|
|
print(f"Loaded {len(schemas)} tool schemas from {SCHEMA_DIR}")
|
|
|
|
py_code = generate_python(schemas)
|
|
# NICHT nur auf das Verzeichnis pruefen — das existiert fast immer und der
|
|
# Generator schrieb dann in ein beliebiges Nachbarverzeichnis (gemessen).
|
|
# Der Marker macht daraus eine echte Aussage: liegt hier das Agenten-Repo?
|
|
if (PY_OUTPUT.parent / "tools.py").is_file():
|
|
PY_OUTPUT.write_text(py_code, encoding="utf-8")
|
|
print(f"Generated Python: {PY_OUTPUT} ({len(py_code)} bytes)")
|
|
else:
|
|
print(f"Skipped Python output: {PY_OUTPUT.parent} not found "
|
|
f"(agent repo not checked out next to this one)")
|
|
|
|
ts_code = generate_typescript(schemas)
|
|
TS_OUTPUT.parent.mkdir(parents=True, exist_ok=True)
|
|
TS_OUTPUT.write_text(ts_code, encoding="utf-8")
|
|
print(f"Generated TypeScript: {TS_OUTPUT} ({len(ts_code)} bytes)")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|