Wire a tool into the schema-driven AI Tools panel
The “AI Tools” panel in saas-2.0 is not
hand-authored. There is no per-tool React component, no form JSX, no config file you
have to edit. The panel is built at runtime from each tool’s registered Pydantic JSON
Schema — the same input_schema_json the agent plans against. Ship a tool worker with a
typed input model and it shows up in the panel with a working form. That is the whole
integration: there is no frontend step.
The contract chain: schema in, form out
Section titled “The contract chain: schema in, form out”The schema makes six hops from your Python model to a rendered control. Every link is a committed, verifiable path — nothing is inferred or guessed:
| # | Where | What happens |
|---|---|---|
| 1 | Worker | BaseTool.input_schema_json() returns json.dumps(self.input_model.model_json_schema()) and rides RegisterTool on boot. |
| 2 | generation.tools row |
Stored as the input_schema jsonb column (Tool.input_schema_json, field 7). Re-register upserts ON CONFLICT (environment, model, operation). |
| 3 | GetActiveTools |
The Go data service returns active rows for the environment, cached under tools:active:{environment}. RegisterTool invalidates that key, so a reboot refreshes the panel. |
| 4 | Gateway ListTools |
core-gateway-consumer projects each internal Tool down to a consumer-safe ToolView and forwards input_schema_json verbatim. |
| 5 | BFF GET /api/tools |
The Next.js route proxies GenerationService.ListTools and returns the ToolView rows as JSON. |
| 6 | Panel | parseToolSchema() turns the JSON Schema into panel controls; buildPayload() coerces the entered values back to the Pydantic-typed payload for createGeneration. |
The two protos that define the contract:
- Registry side —
GenerationToolDataServiceingeneration/tool/v1/tool.proto.Toolcarriesinput_schema_json(field 7,db_column "input_schema") andoutput_schema_json(field 8).GetActiveToolsisrequire_internal_auth,target_db = "prod", cached 3600s undertools:active:{environment}. - Consumer side —
GenerationService.ListToolsinconsumer/v1/consumer.proto.ToolViewexposes onlyid, name, operation, model, description, cost_unit, input_schema_json— no credentials, pricing, or rate-limit internals. The response is a flat repeated list; grouping is a UI concern, never in the proto.
The gateway projection is a one-liner —
mappers.py::tool_view
copies input_schema_json=tool.input_schema_json, and
servicers/generation.py::ListTools
calls GetActiveTools with the gateway’s own environment.
How the form is built
Section titled “How the form is built”src/panels/schema.ts
(parseToolSchema) walks the schema’s properties and maps each one to a control. It
resolves $ref / allOf / anyOf (so X | None optionals and Pydantic $defs enums
work), then applies these rules:
| JSON Schema shape | Rendered control |
|---|---|
x-sv-file-input marker (single or list item) |
Library file picker (submits the picked file id) |
enum |
Select (numeric enums coerced back to numbers) |
boolean |
Toggle |
integer / number with minimum and maximum |
Slider (else a number input) |
object (or nested properties) |
JSON textarea |
array of enum |
Tag multi-select |
| array of objects | JSON-array textarea |
string whose name/description hints at a media file |
Upload |
long / prompt-named string (prompt, lyrics, description, …) |
Multi-line prompt |
any other string |
Text input |
buildPayload() is the inverse: it coerces each entered value back to the Pydantic-expected
JSON type before createGeneration — numbers to numbers, JSON textareas parsed, file
pickers reduced to file id(s), empty optionals dropped.
Grouping and submit
Section titled “Grouping and submit”-
One card per operation.
libraryScript.js::loadTools()fetches the registry vialistToolsCached()(GET /api/tools) and groups the flat list byoperationinto abackendOpsmap —operation → { label, description, models[] }. Each operation becomes one card. -
Models become a dropdown. Tools that share an
operationbut differ bymodel(e.g. several song-gen versions) become the selectable Model dropdown insideDynamicToolPanel.tsx. Switching the model rebuilds the form from that tool’s schema. -
Submit runs a real generation. On submit,
buildPayloadproduces the payload and the panel callscreateGeneration({ toolId, projectId, workspaceId, payloadJson, license }), then dispatches asv:generation-startedevent the app shell can open a live stream from.
Related
Section titled “Related”- Add a new generation tool worker — where the input model (and thus the form) is written
- Add pricing to a tool — the other must-do so the tool isn’t rejected at call time
- core-gateway-consumer — the
ListTools/ToolViewprojection - The contract: proto as source of truth — why the schema is authoritative end to end
- BFF routes → RPC map —
GET /api/toolsand the rest of the BFF surface - Tool catalog — every operation and model currently registered