From 811a70eebb03879fca8357f697fc8122154b06e0 Mon Sep 17 00:00:00 2001 From: Kentaro Wakayama Date: Thu, 6 Aug 2026 13:15:39 +0200 Subject: [PATCH] chore(templates): make the starter eval readable The ai-agent starter eval is the first eval most people ever read. It opened with four hand-rolled lookaround regexes: metrics.answer.regex({ pattern: String.raw`(? { ]); }); - it("accepts sentence punctuation without accepting longer monetary values", async () => { + it("grades the starter's money answer with a rubric a reader can follow", async () => { + // The starter eval is the first eval most people ever read, so it has to be + // legible. It used to gate each amount with a hand-rolled lookaround regex + // (`(? metric.name), [ - "answer.regex", - "answer.regex", - "answer.regex", - "answer.regex", + + assertEquals(assistantEval.metrics.map((metric) => metric.name), [ + "agent.calledTool", + "agent.noFailedTools", + "judge.rubric", ]); - const createRecord = (text: string): EvalRecord => ({ - id: "calculator:1", - evalId: "eval:assistant", - exampleId: "calculator", - repetition: 1, - input: "Calculate the tip and split.", - output: { text }, - reference: "$99.71 total; two people pay $33.24 and one pays $33.23.", - metadata: {}, - trace: { events: [], toolCalls: [] }, - usage: {}, - durationMs: 1, - completed: true, - }); - const validResults = await Promise.all( - moneyMetrics.map((metric) => - metric.evaluate( - createRecord( - "The tip is $15.21. The total is $99.71. Two people pay $33.24, and one pays $33.23.", - ), - ) - ), + const source = await Deno.readTextFile( + new URL("./files/ai-agent/evals/assistant.eval.ts", import.meta.url), + ); + assertEquals( + source.includes("metrics.answer.regex"), + false, + "the starter eval should not teach hand-rolled regex assertions", + ); + assertEquals( + source.includes("String.raw"), + false, + "the starter eval should not need raw strings to express an assertion", ); - assertEquals(validResults.map((result) => result.pass), [true, true, true, true]); - const tipMetric = moneyMetrics[0]; - assertExists(tipMetric); - for (const valid of ["$15.21.", String.raw`\$15.21`, "($15.21)", "**$15.21**"]) { - assertEquals((await tipMetric.evaluate(createRecord(valid))).pass, true); + // Dropping the regexes moved exactness onto the judge, so the rubric has to + // spell out both the amounts and that near-misses fail. + const rubricMetric = assistantEval.metrics.at(-1); + assertExists(rubricMetric); + const rubric = String(rubricMetric.config?.rubric ?? ""); + for (const amount of ["$15.21", "$99.71", "$33.24", "$33.23"]) { + assertEquals( + rubric.includes(amount), + true, + `the rubric should name the expected ${amount}`, + ); } - for ( - const invalid of [ - "-15.21", - "-$15.21", - String.raw`-\$15.21`, - "115.21", - "$15.210", - "$15.21.0", - ] - ) { - assertEquals((await tipMetric.evaluate(createRecord(invalid))).pass, false); + assertEquals( + /exact/i.test(rubric), + true, + "the rubric should require exact amounts now that no regex enforces it", + ); + for (const nearMiss of ["$33.2366", "$133.23"]) { + assertEquals( + rubric.includes(nearMiss), + true, + `the rubric should show ${nearMiss} as a failing near-miss`, + ); } }); diff --git a/cli/templates/manifest.json b/cli/templates/manifest.json index 7559281641..ba55bfdf5a 100644 --- a/cli/templates/manifest.json +++ b/cli/templates/manifest.json @@ -27,7 +27,7 @@ "app/layout.tsx": "import \"../globals.css\";\nimport { Head } from \"veryfront/head\";\n\nexport default function RootLayout({\n children,\n}: {\n children: React.ReactNode;\n}): React.ReactNode {\n return (\n <>\n \n Assistant\n \n \n {children}\n \n );\n}\n", "app/markdown-renderer.tsx": "\"use client\";\n\nimport ReactMarkdown from \"react-markdown\";\nimport remarkGfm from \"remark-gfm\";\nimport type { MarkdownRendererProps } from \"veryfront/markdown\";\n\n/**\n * Rich Markdown for assistant answers.\n *\n * `veryfront/markdown` presents plain source until a renderer is installed, so\n * this component supplies one. Swap in any renderer that accepts\n * `MarkdownRendererProps` to change how answers are parsed and rendered.\n */\nexport function MarkdownRenderer({ source }: MarkdownRendererProps): React.JSX.Element {\n return {source};\n}\n", "app/page.tsx": "\"use client\";\n\nimport { Chat } from \"veryfront/chat\";\nimport { MarkdownRendererProvider } from \"veryfront/markdown\";\nimport { MarkdownRenderer } from \"./markdown-renderer.tsx\";\n\nexport default function ChatPage(): React.JSX.Element {\n return (\n \n \n \n );\n}\n", - "evals/assistant.eval.ts": "import { datasets, evalAgent, judges, metrics } from \"veryfront/eval\";\n\nexport default evalAgent({\n name: \"Assistant smoke test\",\n target: \"agent:assistant\",\n dataset: datasets.inline([\n {\n id: \"calculator\",\n input:\n \"Calculate an 18% tip on $84.50, split the total among three people, and explain the result briefly.\",\n reference: \"$99.71 total; two people pay $33.24 and one pays $33.23.\",\n },\n ]),\n metrics: [\n metrics.answer.regex({\n pattern: String.raw`(?\n \n \n\n", "README.md": "# AI Agent\n\nA small, customizable agent with a streaming chat UI and tool support.\n\n## What's included\n\n- Single assistant agent with streaming chat UI\n- Example calculator tool\n- Smoke eval for the agent and calculator\n- App-mode `Chat` component for real-time responses\n\n## Structure\n\n```\nagents/assistant.ts Agent definition\ntools/calculator.ts Example tool\nevals/assistant.eval.ts Agent smoke eval\napp/\n api/ag-ui/route.ts AG-UI endpoint\n page.tsx Chat interface\n```\n\n## Customize\n\n- Edit `agents/assistant.ts` to change the agent's identity, instructions, and suggestions.\n- Add or replace files in `tools/` to give the agent new capabilities.\n- Update `evals/assistant.eval.ts`, then run `npm run eval -- assistant`.\n- Edit `app/page.tsx` when you need to customize the chat UI.\n",