-
Notifications
You must be signed in to change notification settings - Fork 18
feat(studio): new example agent, eval modal supports dataset-driven, per-row results view #1059
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
a72b336
feat(studio): add CardSelect component; update DataView toolbar and t…
nv-odrulea b40960c
feat(studio): add dataset-driven eval submission and results view
nv-odrulea 4515695
chore(studio): remove old locations of files moved to components/eval…
nv-odrulea 2785f31
chore(studio): remove stale submitEvaluationSpec.test.ts from old loc…
nv-odrulea 99ffa5f
fix(studio): add agent example files, drop CardSelect
nv-odrulea 9397034
fix(studio): coderabbit findings fixed
nv-odrulea 987bdff
fix(studio): coderabbit findings fixed
nv-odrulea 4f7ce6a
fix(studio): add guard to check array type before map
nv-odrulea 29aee2f
fix(studio): address comments
nv-odrulea b181f82
fix(studio): address comments
nv-odrulea 2d0e942
fix(studio): extract var to constant
nv-odrulea a73d855
fix(studio): update react-router imports, dedupe cancelAgentEval
nv-odrulea File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
120 changes: 120 additions & 0 deletions
120
web/packages/studio/public/sample-agents/email-security-analyst/agent.yml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,120 @@ | ||
| # Studio sample agent: email security analyst tool-calling agent. | ||
| # Served as a static asset and fetched by Studio's Create Example Agent flow. | ||
| # The `_type` values below require the installed nemo-agents-example-email-security | ||
| # plugin. model_name is overwritten with the user's selected model at create time | ||
| # (parse-then-set), so the value here is only a default. | ||
| # | ||
| # This agent is the TASK-DRIVEN evaluation example. It models an analyst-facing | ||
| # assistant inside a mail client: the operator selects one or more messages and | ||
| # optionally types a question. The eval's heterogeneity comes from how the human | ||
| # engages -- one message or six, question or none, "is this safe?" vs "write a | ||
| # warning for the team" -- not from artificially varied material. | ||
| # eval-config.dataset-driven.json is the dataset-driven counterpart. | ||
| # | ||
| # `return_direct` lists every tool, which ends the graph on the tool result rather | ||
| # than running a second generation over it. That is what makes each tool's | ||
| # first-line output contract a guarantee; without it the model rewrites the answer | ||
| # and every deterministic metric breaks. | ||
| # | ||
| # Requires a model that supports native tool calls. Probed 2026-07-29: every model | ||
| # reachable through the gateway emitted tool_calls (nemotron nano/super/ultra/mini, | ||
| # nano-9b-v2, azure gpt-4.1-mini). Unreachable models fail loudly with a 404 at | ||
| # invoke time rather than degrading silently. | ||
|
|
||
| functions: | ||
| review_messages: | ||
| _type: review_messages | ||
| llm: llm | ||
| triage_message: | ||
| _type: triage_message | ||
| llm: llm | ||
| triage_batch: | ||
| _type: triage_batch | ||
| llm: llm | ||
| attribute_attack: | ||
| _type: attribute_attack | ||
| llm: llm | ||
| assess_severity: | ||
| _type: assess_severity | ||
| llm: llm | ||
| trace_thread: | ||
| _type: trace_thread | ||
| llm: llm | ||
| analyze_headers: | ||
| _type: analyze_headers | ||
| llm: llm | ||
| check_url_brand: | ||
| _type: check_url_brand | ||
| llm: llm | ||
| incident_response: | ||
| _type: incident_response | ||
| llm: llm | ||
| draft_warning: | ||
| _type: draft_warning | ||
| llm: llm | ||
| extract_iocs: | ||
| _type: extract_iocs | ||
| llms: | ||
| llm: | ||
| _type: openai | ||
| api_key: not-used | ||
| model_name: default/nvidia-nemotron-3-nano-30b-a3b | ||
| temperature: 0.0 | ||
| max_tokens: 4096 | ||
| workflow: | ||
| _type: tool_calling_agent | ||
| tool_names: | ||
| [ | ||
| review_messages, | ||
| triage_message, | ||
| triage_batch, | ||
| attribute_attack, | ||
| assess_severity, | ||
| trace_thread, | ||
| analyze_headers, | ||
| check_url_brand, | ||
| incident_response, | ||
| draft_warning, | ||
| extract_iocs, | ||
| ] | ||
| return_direct: | ||
| [ | ||
| review_messages, | ||
| triage_message, | ||
| triage_batch, | ||
| attribute_attack, | ||
| assess_severity, | ||
| trace_thread, | ||
| analyze_headers, | ||
| check_url_brand, | ||
| incident_response, | ||
| draft_warning, | ||
| extract_iocs, | ||
| ] | ||
| llm_name: llm | ||
| additional_instructions: >- | ||
| You are an email security analyst assistant inside a mail client. | ||
|
|
||
| Your input is a JSON object with exactly two keys. `user_message` is what the | ||
| analyst typed, and is an empty string when they typed nothing. `emails` is a | ||
| list of the messages they selected, and is empty when they selected none. | ||
|
|
||
| Pick exactly one tool. When `user_message` is empty, use review_messages -- | ||
| the analyst wants a general review of what they selected. Otherwise choose the | ||
| tool whose description matches what they are asking for, and pass it the | ||
| material it needs: the selected messages, the question, or both. | ||
|
|
||
| Refer to messages by their 1-based position in `emails`: the first is 1, the | ||
| second is 2, and so on. | ||
|
|
||
| Input that is not that JSON object is the request itself. Read any question it | ||
| contains as the analyst's ask and treat the remaining material as the selected | ||
| message, then pick a tool the same way. Never refuse or ask for a different | ||
| format. | ||
| general: | ||
| telemetry: | ||
| tracing: | ||
| nemo_trace: | ||
| _type: nemo_files | ||
| # workspace and agent_name are injected at deploy time | ||
| batch_size: 128 |
40 changes: 40 additions & 0 deletions
40
web/packages/studio/public/sample-agents/email-security-analyst/dataset.jsonl
Large diffs are not rendered by default.
Oops, something went wrong.
85 changes: 85 additions & 0 deletions
85
...ublic/sample-agents/email-security-analyst/eval-config.dataset-driven.README.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| # Dataset-driven evaluation — Email Security Analyst | ||
|
|
||
| This fileset holds the **dataset-driven** eval config seeded from the built-in Email | ||
| Security Analyst sample, plus the dataset and this note. It is yours to edit; nothing | ||
| regenerates it. | ||
|
|
||
| Dataset-driven means **one metric set scores every row** of a fixed dataset. The sibling | ||
| task-driven config scores the same agent a different way: a set of distinct tasks, each | ||
| carrying its own metrics. | ||
|
|
||
| ## What is being evaluated | ||
|
|
||
| One question, asked 40 times over 40 different emails: can the agent tell a phishing | ||
| message from a legitimate one? | ||
|
|
||
| This measures a single capability in depth, where the task-driven suite measures breadth. | ||
| Same agent, same subject matter, different lens — which is the point of shipping both. | ||
|
|
||
| ## The dataset | ||
|
|
||
| `dataset.jsonl` — 40 rows, balanced 20 phishing / 20 benign. Each row: | ||
|
|
||
| ```json | ||
| { "subject": "...", "sender": "...", "body": "...", "label": "phishing" } | ||
| ``` | ||
|
|
||
| `label` is the ground truth and is never shown to the agent. | ||
|
|
||
| The rows are deliberately contestable, not textbook. Phishing rows are calmly written and | ||
| correctly branded, with a lookalike domain or an out-of-band request as the only tell. | ||
| Benign rows look alarming on the surface — invoices, password resets, wire transfers — but | ||
| come from consistent senders and ask for nothing. Rows that leaked their own label, ran too | ||
| short, or duplicated a subject were filtered out. | ||
|
|
||
| ## How a row reaches the agent | ||
|
|
||
| `prompt_template` renders each row into a question plus the message: | ||
|
|
||
| ```text | ||
| Is this legit? | ||
|
|
||
| Subject: {{ item.subject }} | ||
| From: {{ item.sender }} | ||
|
|
||
| {{ item.body }} | ||
| ``` | ||
|
|
||
| That question is what routes the agent to its triage capability, which answers with a bare | ||
| `phishing` or `benign` on the first line and its reasoning after. | ||
|
|
||
| ## The metric | ||
|
|
||
| | Metric | Output | Range | Checks | | ||
| | ----------- | -------- | ----- | ------------------------------------------ | | ||
| | `llm-judge` | accuracy | 0–1 | First line of the response matches `label` | | ||
|
|
||
| The judge reads the **first line**, not the last. The agent answers first and explains | ||
| after, so its reasoning routinely names the opposite verdict — a judge told to take the | ||
| "final" verdict would score correct answers as wrong. | ||
|
|
||
| ## Reading the results | ||
|
|
||
| Expect a mid-range score, not a perfect one; the dataset was built to leave headroom. A | ||
| recent baseline run scored **0.63** with a clear pattern: of 14 errors, 12 were **false | ||
| positives** — benign mail called phishing. The agent is a security assistant told to hunt | ||
| social-engineering signals, so it leans cautious. That asymmetry is more interesting than | ||
| the headline number and is worth checking on your own runs. | ||
|
nv-odrulea marked this conversation as resolved.
|
||
|
|
||
| A near-zero score means something mechanical, not a collapsed agent: most likely the judge | ||
| reading the wrong line, or the agent's tool missing from the workflow's `return_direct`, so | ||
| a second generation rewrote the answer away from the first-line contract. | ||
|
|
||
| ## Editing this config | ||
|
|
||
| `dataset` in this fileset already points at the copy of `dataset.jsonl` sitting beside it — | ||
| it is rewritten at seed time so the config and its data travel together. The published | ||
| template instead carries a placeholder, `<workspace>/<fileset>#dataset.jsonl`, because no | ||
| such fileset exists in a fresh workspace; anything running that template directly, rather | ||
| than through the Run Evaluation flow, must set a real reference first. | ||
|
|
||
| If you point it elsewhere, keep the four field names — the `prompt_template` and the judge | ||
| both reference them. | ||
|
|
||
| Metric templates get string operations only; there is no JSON parsing, which is why the | ||
| answer is positional rather than structured. | ||
60 changes: 60 additions & 0 deletions
60
...ckages/studio/public/sample-agents/email-security-analyst/eval-config.dataset-driven.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| { | ||
| "dataset": "<workspace>/<fileset>#dataset.jsonl", | ||
| "prompt_template": "Is this legit?\n\nSubject: {{ item.subject }}\nFrom: {{ item.sender }}\n\n{{ item.body }}", | ||
| "metrics": [ | ||
| { | ||
| "bundle_kind": "metric-bundle", | ||
| "bundle_format_version": "v1", | ||
| "metric_type": "llm-judge", | ||
| "metadata": { | ||
| "description": "Verdict accuracy vs the dataset's label column.", | ||
| "labels": {} | ||
| }, | ||
| "outputs": [ | ||
| { | ||
| "name": "accuracy", | ||
| "description": null, | ||
| "value_json_schema": { | ||
| "description": "Continuous numeric metric value.", | ||
| "title": "ContinuousScore", | ||
| "type": "number" | ||
| } | ||
| } | ||
| ], | ||
| "secrets": {}, | ||
| "payload": { | ||
| "kind": "inline", | ||
| "metric": { | ||
| "type": "llm-judge", | ||
| "model": "default/nvidia-nemotron-3-super-120b-a12b", | ||
| "prompt_template": { | ||
| "messages": [ | ||
| { | ||
| "role": "user", | ||
| "content": "You are scoring an email security analyst. Ground-truth verdict: \"{{ item.label }}\". The analyst's answer is on the FIRST LINE of the response; everything after it is supporting explanation and must be ignored, even if it mentions the other verdict.\n\nAgent response:\n{{ sample.output_text }}\n\nRespond with a JSON object {\"accuracy\": <value>} where accuracy is 1 if the first line matches the expected verdict, else 0." | ||
| } | ||
| ] | ||
| }, | ||
| "scores": [ | ||
| { | ||
| "name": "accuracy", | ||
| "minimum": 0, | ||
| "maximum": 1 | ||
| } | ||
| ], | ||
| "inference": { | ||
| "max_tokens": 1024, | ||
| "extra_body": { | ||
| "nvext": { | ||
| "max_thinking_tokens": 256 | ||
| } | ||
| } | ||
| }, | ||
| "reasoning": { | ||
| "end_token": "</think>" | ||
| } | ||
| } | ||
| } | ||
| } | ||
| ] | ||
| } |
75 changes: 75 additions & 0 deletions
75
...o/public/sample-agents/email-security-analyst/eval-config.task-driven.README.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| # Task-driven evaluation — Email Security Analyst | ||
|
|
||
| This fileset holds the **task-driven** eval config seeded from the built-in Email Security | ||
| Analyst sample, plus this note. It is yours to edit; nothing regenerates it. | ||
|
|
||
| Task-driven means each task carries **its own metrics**, so one suite can grade work of | ||
| different kinds. The sibling dataset-driven config scores the same agent a different way: | ||
| one metric set over every row of a fixed dataset. | ||
|
|
||
| ## What is being evaluated | ||
|
|
||
| The agent is an analyst-facing assistant inside a mail client. The operator selects one or | ||
| more messages and optionally types a question. Input arrives as one JSON object: | ||
|
|
||
| ```json | ||
| { "user_message": "is this legit?", "emails": ["Subject: ...\nFrom: ...\n\n<body>"] } | ||
| ``` | ||
|
|
||
| `user_message` is `""` when nothing was typed. `emails` is `[]` when nothing was selected. | ||
|
|
||
| The agent routes that request to one of nine capability tools and returns the tool's answer | ||
| verbatim. **Routing is the thing under test.** No task tells the agent which capability to | ||
| use or how to format its reply — a real inbox does not arrive with instructions, so the | ||
| taxonomy (attack types, severity bands, report format) lives in the agent, not the prompt. | ||
|
|
||
| Each tool answers first and explains after: the first line is the bare answer, reasoning | ||
| follows. That is what lets deterministic metrics read a result without an LLM. | ||
|
|
||
| Variety here comes from _how a human engages_ — one message or six, question or no question | ||
| — rather than from artificially varied material. | ||
|
|
||
| ## The 22 tasks | ||
|
|
||
| Two per family. | ||
|
|
||
| | Task | Ask | Metric | Checks | | ||
| | -------------- | ------------------------------------ | ---------------------------------- | ------------------------------------------------------------ | | ||
| | classify | _is this legit?_ | `llm-judge` accuracy | Verdict on line 1 matches ground truth | | ||
| | attribution | _what kind of attack is this?_ | `exact-match` | Attack type against a closed set | | ||
| | severity | _how serious is this?_ | `llm-judge` severity_match | Ordinal distance: exact 1.0, one band off 0.5, two off 0 | | ||
| | batch | _which should I quarantine?_ | `f1` | Overlap between chosen and correct message positions | | ||
| | thread | _where did this thread go bad?_ | `number-check` | Position of the injected message | | ||
| | headers | _why did this fail authentication?_ | `exact-match` | Which of SPF / DKIM / DMARC failed | | ||
| | url | _who is this link pretending to be?_ | `string-check` | Impersonated brand appears in the answer | | ||
| | default-review | _(no question)_ | `string-check` + `llm-judge` | Routing, per-message verdicts, field completeness | | ||
| | incident | _what do we do?_ | `llm-judge` incident_steps | Coverage of expected remediation steps | | ||
| | injection | _is this legit?_ | `llm-judge` injection_resisted | Correct verdict despite an instruction embedded in the email | | ||
| | draft | _write a warning for the team_ | `llm-judge` draft_quality (rubric) | poor / adequate / strong against expected elements | | ||
|
|
||
| **default-review** is the only pair with no question, and its `string-check` is a routing | ||
| assertion: only the general-review tool heads its output with `ANALYSIS`, so the check fails | ||
| if the agent picks a different tool. | ||
|
|
||
| **draft** is the only pair with no single correct answer — a staff warning has no exact | ||
| wording. Its rubric grades how many expected elements appear, so its `reference` holds those | ||
| elements rather than an expected reply. | ||
|
|
||
| ## Reading the results | ||
|
|
||
| Prefer the per-`view` scores over raw metric rows: a view is scoped to its task, whereas raw | ||
| rows union output names across all tasks, so a metric used by two tasks reads `NaN` for the | ||
| other twenty. | ||
|
|
||
| Before reading a low score as a weak agent, rule out the mechanical causes: a tool missing | ||
| from the workflow's `return_direct` (a second generation rewrites the answer and breaks the | ||
| first-line contract), a judge prompt describing an older output format, and mis-routing — | ||
| right answer, wrong tool, wrong contract. All three look identical in the score table. | ||
|
|
||
| ## Editing this config | ||
|
|
||
| Metric templates get string operations only; there is no JSON parsing, which is why answers | ||
| are positional rather than structured. A task may not carry two metrics of the same type — | ||
| use one metric with several outputs instead. Every judge that reads a verdict must read the | ||
| **first line**, not the last: reasoning follows the answer and often mentions the opposite | ||
| verdict. |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.