feat(ai/dialect): added jsdoc-style tool example rendering for inventory
- Add `renderToolExamplesJsdoc` to generate `@example` comment lines for tool inventories. - Extract `bareStringArg` helper to simplify single-argument checks across renderers. - Update tool inventory rendering to use the new JSDoc example format.
This commit is contained in:
@@ -15,17 +15,12 @@ export function renderToolExamples(tool: InbandTool, intentField?: string): stri
|
||||
const examples = tool.examples;
|
||||
if (!examples?.length) return "";
|
||||
const renderCall = (args: Record<string, unknown>): string => {
|
||||
let soleKey: string | undefined;
|
||||
let argCount = 0;
|
||||
for (const key in args) {
|
||||
argCount++;
|
||||
soleKey = key;
|
||||
}
|
||||
if (argCount === 1 && soleKey !== undefined && typeof args[soleKey] === "string") {
|
||||
const bare = bareStringArg(args);
|
||||
if (bare !== undefined) {
|
||||
// Bare payload. The intent placeholder still rides on the envelope so
|
||||
// intent-traced schemas (where `i` is required) keep teaching it.
|
||||
const intentAttr = intentField ? ` ${intentField}="${INTENT_PLACEHOLDER}"` : "";
|
||||
return `<example${intentAttr}>\n${args[soleKey]}\n</example>`;
|
||||
return `<example${intentAttr}>\n${bare}\n</example>`;
|
||||
}
|
||||
// When intent tracing injects `i` into the schema, examples must show a
|
||||
// placeholder so the model learns to emit it. Keep it first, matching the
|
||||
@@ -43,3 +38,34 @@ export function renderToolExamples(tool: InbandTool, intentField?: string): stri
|
||||
});
|
||||
return `<examples>\n${parts.join("\n")}\n</examples>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a tool's examples as JSDoc-style `@example` lines for comment-gutter
|
||||
* contexts (the Harmony `namespace functions` inventory): `@example "caption"`
|
||||
* followed by the call in the same Python kwargs syntax as the wire block. The
|
||||
* tag line delimits each example, so no XML envelope is needed — which is why
|
||||
* the inventory uses this instead of `//`-prefixing the `<examples>` block.
|
||||
*/
|
||||
export function renderToolExamplesJsdoc(tool: InbandTool): string {
|
||||
const examples = tool.examples;
|
||||
if (!examples?.length) return "";
|
||||
const renderCall = (args: Record<string, unknown>): string => bareStringArg(args) ?? pyCall(tool.name, args);
|
||||
const parts = examples.map(ex => {
|
||||
const head = ex.caption ? `@example ${JSON.stringify(ex.caption)}` : "@example";
|
||||
if ("call" in ex) return `${head}\n${renderCall(ex.call)}`;
|
||||
if ("good" in ex) return `${head}\nWRONG:\n${renderCall(ex.bad)}\nRIGHT:\n${renderCall(ex.good)}`;
|
||||
return ex.note ? `${head}\n${ex.note}` : head;
|
||||
});
|
||||
return parts.join("\n");
|
||||
}
|
||||
|
||||
/** Sole-argument string payload, if the call has exactly one string argument. */
|
||||
function bareStringArg(args: Record<string, unknown>): string | undefined {
|
||||
let sole: unknown;
|
||||
let count = 0;
|
||||
for (const key in args) {
|
||||
count++;
|
||||
sole = args[key];
|
||||
}
|
||||
return count === 1 && typeof sole === "string" ? sole : undefined;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { jsonSchemaToTypeScript, toolWireSchema } from "../utils/schema";
|
||||
import { renderToolExamples } from "./examples";
|
||||
import { renderToolExamplesJsdoc } from "./examples";
|
||||
import type { InbandTool } from "./types";
|
||||
|
||||
/**
|
||||
@@ -18,7 +18,7 @@ export function renderToolInventory(tools: readonly InbandTool[]): string {
|
||||
if (description) {
|
||||
for (const line of description.split("\n")) lines.push(`// ${line}`.trimEnd());
|
||||
}
|
||||
const examples = renderToolExamples(tool);
|
||||
const examples = renderToolExamplesJsdoc(tool);
|
||||
if (examples) {
|
||||
if (description) lines.push("//");
|
||||
for (const line of examples.split("\n")) lines.push(`// ${line}`.trimEnd());
|
||||
|
||||
@@ -26,15 +26,14 @@ describe("renderToolInventory", () => {
|
||||
expect(out).toContain("});");
|
||||
});
|
||||
|
||||
it("renders examples as comment lines above the declaration", () => {
|
||||
it("renders examples as @example comment lines above the declaration", () => {
|
||||
const out = renderToolInventory([searchTool]);
|
||||
expect(out).toContain("// <examples>");
|
||||
expect(out).toContain("// # Basic");
|
||||
expect(out).toContain('// @example "Basic"');
|
||||
expect(out).toContain('// web_search(query="rust", recency="week")');
|
||||
expect(out).toContain("// </examples>");
|
||||
expect(out).not.toContain("<examples>");
|
||||
// Examples sit between the description and the type declaration.
|
||||
expect(out.indexOf("// Searches the web.")).toBeLessThan(out.indexOf("// <examples>"));
|
||||
expect(out.indexOf("// </examples>")).toBeLessThan(out.indexOf("type web_search"));
|
||||
expect(out.indexOf("// Searches the web.")).toBeLessThan(out.indexOf('// @example "Basic"'));
|
||||
expect(out.indexOf('// @example "Basic"')).toBeLessThan(out.indexOf("type web_search"));
|
||||
});
|
||||
|
||||
it("omits the examples comment block when a tool has none", () => {
|
||||
@@ -45,7 +44,7 @@ describe("renderToolInventory", () => {
|
||||
};
|
||||
const out = renderToolInventory([tool]);
|
||||
expect(out).toContain("type noop = (_: {");
|
||||
expect(out).not.toContain("<examples>");
|
||||
expect(out).not.toContain("@example");
|
||||
});
|
||||
|
||||
it("renders a parameterless tool without an args object", () => {
|
||||
|
||||
Reference in New Issue
Block a user