Repository navigation
feat(citation_if): add citation_if resources server for citation instruction-following - #2586
Conversation
β¦ruction-following Signed-off-by: Kavya Nayaka <knayaka@nvidia.com>
| - nemo_gym: Apache 2.0 | ||
|
|
||
| The example rows in `data/example.jsonl` are synthetic, written for this server. No external | ||
| corpus is redistributed here. |
There was a problem hiding this comment.
if any data can be made public that would be great, or a data prep example, either via prepare.py
There was a problem hiding this comment.
The training corpus is internal and can't be published. I can add a prepare.py that generates synthetic rows in this format - covering the nine citation grammars, four instruction placements, both ID granularities and the no_cite inversion (bit heavy work) - so the server is usable end to end without our data. Would that satisfy the ask, or would you prefer something lighter, e.g. documenting the row schema and generation procedure in the README?
There was a problem hiding this comment.
Added a "Building your own data" section in the README covering the row schema, trajectory shape, and generation procedure, with data/example.jsonl as a working instance.
jkyi-nvidia
left a comment
There was a problem hiding this comment.
Looks great let's just make sure the data includes guards for max tool calls allowed somewhere and write that point down explicitly in README so that it is not forgotten and noted.
β¦ta, not the server Signed-off-by: Kavya Nayaka <knayaka@nvidia.com>
β¦nv checks are added Signed-off-by: Kavya Nayaka <knayaka@nvidia.com>
β¦tead of the unpublished corpus Signed-off-by: Kavya Nayaka <knayaka@nvidia.com>
dd39ca2 to
5bdb9b2
Compare
|
/claude review |
|
SHIP β LGTM, no reliability concerns. Reviewed the full diff, Why this is low-risk:
Nothing material to flag. |
Signed-off-by: Kavya Nayaka <knayaka@nvidia.com>
β¦ruction-following (#2586) ## What does this PR do? Closes #2585 Adds `resources_servers/citation_if`, a resources server that grades **citation instruction-following**: did the model cite in the grammar it was told to use, cite only IDs that exist in its retrieved context, and cite the ones that were required β and did it actually answer. CPU-only string matching, one `/verify` endpoint, binary reward; no model calls, no GPU. Each row is a complete frozen conversation β system instruction naming a citation grammar, a question, search rounds, then a final user turn that closes retrieval and asks for synthesis. The policy answers once. ### Verification logic Pure string matching in `scorer.py` (no FastAPI dependency, importable standalone). Gate sequence in cite mode: | # | gate | fails when | |---|---|---| | 0 | `structural` | empty final text, or the policy emitted a tool call | | 0b | `no_answer` | citation markup only β no word character survives once citations are removed | | 1 | `malformed_attempt` | after stripping well-formed spans, any residual ID-shaped token remains | | 2 | `must_cite` | fewer than `min_valid_citations` valid citations | | 3 | `no_hallucination` | a parsed ID is not in `valid_id_set` | | 4 | `correctness` | a required ID was missed, or `|cited| > |expected| + expected_slack` | `mode=no_cite` inverts gates 1-4 (reward 1 iff zero citations of any shape), which is how abstention rows are expressed. 9 citation grammars; `id_regex` is authoritative per row, so random-ID and numeric-ID schemes coexist in one pool. Two further grammars are marked `holdout: True` and must never be assigned to training rows β they exist to detect grid overfitting. **The answer's content is deliberately never graded.** The reward is for instruction-following and the data carries no gold answer. But an answer must *exist* β see gate 0b. Those two properties are locked by an adjacent pair of tests so a future "fix" to one cannot silently undo the other. ### Source of prompts/tasks The committed `data/example.jsonl` (5 rows) is **synthetic β hand-written for this server**, covering four grammars, both `id_kind` values, and one `no_cite` row. It is a contract example, not a difficulty example, and the README says so. The training corpus this reward was developed against is internal and **is not included**. Nothing in the server depends on it; `configs/citation_if.yaml` points `train` at a gitignored path. ### Tests β 327 passing - `test_citation_if.py` β hand-written gate tests, one or more per gate and failure mode - `test_reward_hack_matrix.py` β generated matrix, 16 known reward-hack classes x 9 grammars x 2 ID granularities = 265 cells; pass bar is 100% - `test_residual_fuzz.py` β 3 property tests x 1,000 mutations, **both directions**: leak mutations must score 0, benign ones (whitespace, punctuation, unicode quotes) must stay 1. An over-strict gate is as much a regression as a leaky one and much harder to notice. - `test_app.py` β server layer: Responses-API extraction contract and the `/verify` response ### Licensing Code: Apache 2.0. Dependencies: `nemo_gym` (Apache 2.0). No external corpus is redistributed β the shipped example rows are synthetic. ### Notes for review - `verified: false`, per the convention that the flag is maintainer-granted. - The top-level `README.md` change is generated by the `update-readme-table` pre-commit hook, not hand-edited. ## Checklist - [x] I have read the [contributing guidelines](https://docs.nvidia.com/nemo/gym/latest/contribute/development-setup). - [x] The change is focused; unrelated "drive-by" edits are tracked as separate issues/PRs. - [x] Tests added or updated and pass locally, or N/A for docs-only / non-code changes (so CI unit/server checks pass when applicable). - [x] Pre-commit checks pass locally (`pre-commit run --all-files`) (so CI lint/format/copyright pass). - [x] All commits have DCO sign-off (`git commit -s`) (so the DCO check passes). --------- Signed-off-by: Kavya Nayaka <knayaka@nvidia.com> Co-authored-by: Christian Munley <cmunley@nvidia.com> Signed-off-by: Peter Jin <pjin@nvidia.com>
β¦ruction-following (#2586) ## What does this PR do? Closes #2585 Adds `resources_servers/citation_if`, a resources server that grades **citation instruction-following**: did the model cite in the grammar it was told to use, cite only IDs that exist in its retrieved context, and cite the ones that were required β and did it actually answer. CPU-only string matching, one `/verify` endpoint, binary reward; no model calls, no GPU. Each row is a complete frozen conversation β system instruction naming a citation grammar, a question, search rounds, then a final user turn that closes retrieval and asks for synthesis. The policy answers once. ### Verification logic Pure string matching in `scorer.py` (no FastAPI dependency, importable standalone). Gate sequence in cite mode: | # | gate | fails when | |---|---|---| | 0 | `structural` | empty final text, or the policy emitted a tool call | | 0b | `no_answer` | citation markup only β no word character survives once citations are removed | | 1 | `malformed_attempt` | after stripping well-formed spans, any residual ID-shaped token remains | | 2 | `must_cite` | fewer than `min_valid_citations` valid citations | | 3 | `no_hallucination` | a parsed ID is not in `valid_id_set` | | 4 | `correctness` | a required ID was missed, or `|cited| > |expected| + expected_slack` | `mode=no_cite` inverts gates 1-4 (reward 1 iff zero citations of any shape), which is how abstention rows are expressed. 9 citation grammars; `id_regex` is authoritative per row, so random-ID and numeric-ID schemes coexist in one pool. Two further grammars are marked `holdout: True` and must never be assigned to training rows β they exist to detect grid overfitting. **The answer's content is deliberately never graded.** The reward is for instruction-following and the data carries no gold answer. But an answer must *exist* β see gate 0b. Those two properties are locked by an adjacent pair of tests so a future "fix" to one cannot silently undo the other. ### Source of prompts/tasks The committed `data/example.jsonl` (5 rows) is **synthetic β hand-written for this server**, covering four grammars, both `id_kind` values, and one `no_cite` row. It is a contract example, not a difficulty example, and the README says so. The training corpus this reward was developed against is internal and **is not included**. Nothing in the server depends on it; `configs/citation_if.yaml` points `train` at a gitignored path. ### Tests β 327 passing - `test_citation_if.py` β hand-written gate tests, one or more per gate and failure mode - `test_reward_hack_matrix.py` β generated matrix, 16 known reward-hack classes x 9 grammars x 2 ID granularities = 265 cells; pass bar is 100% - `test_residual_fuzz.py` β 3 property tests x 1,000 mutations, **both directions**: leak mutations must score 0, benign ones (whitespace, punctuation, unicode quotes) must stay 1. An over-strict gate is as much a regression as a leaky one and much harder to notice. - `test_app.py` β server layer: Responses-API extraction contract and the `/verify` response ### Licensing Code: Apache 2.0. Dependencies: `nemo_gym` (Apache 2.0). No external corpus is redistributed β the shipped example rows are synthetic. ### Notes for review - `verified: false`, per the convention that the flag is maintainer-granted. - The top-level `README.md` change is generated by the `update-readme-table` pre-commit hook, not hand-edited. ## Checklist - [x] I have read the [contributing guidelines](https://docs.nvidia.com/nemo/gym/latest/contribute/development-setup). - [x] The change is focused; unrelated "drive-by" edits are tracked as separate issues/PRs. - [x] Tests added or updated and pass locally, or N/A for docs-only / non-code changes (so CI unit/server checks pass when applicable). - [x] Pre-commit checks pass locally (`pre-commit run --all-files`) (so CI lint/format/copyright pass). - [x] All commits have DCO sign-off (`git commit -s`) (so the DCO check passes). --------- Signed-off-by: Kavya Nayaka <knayaka@nvidia.com> Co-authored-by: Christian Munley <cmunley@nvidia.com> Signed-off-by: Peter Jin <pjin@nvidia.com>
What does this PR do?
Closes #2585
Adds
resources_servers/citation_if, a resources server that grades citationinstruction-following: did the model cite in the grammar it was told to use, cite only IDs
that exist in its retrieved context, and cite the ones that were required β and did it actually
answer. CPU-only string matching, one
/verifyendpoint, binary reward; no model calls, no GPU.Each row is a complete frozen conversation β system instruction naming a citation grammar, a
question, search rounds, then a final user turn that closes retrieval and asks for synthesis.
The policy answers once.
Verification logic
Pure string matching in
scorer.py(no FastAPI dependency, importable standalone). Gatesequence in cite mode:
structuralno_answermalformed_attemptmust_citemin_valid_citationsvalid citationsno_hallucinationvalid_id_setcorrectnessmode=no_citeinverts gates 1-4 (reward 1 iff zero citations of any shape), which is howabstention rows are expressed. 9 citation grammars;
id_regexis authoritative per row, sorandom-ID and numeric-ID schemes coexist in one pool. Two further grammars are marked
holdout: Trueand must never be assigned to training rows β they exist to detect gridoverfitting.
The answer's content is deliberately never graded. The reward is for
instruction-following and the data carries no gold answer. But an answer must exist β see
gate 0b. Those two properties are locked by an adjacent pair of tests so a future "fix" to
one cannot silently undo the other.
Source of prompts/tasks
The committed
data/example.jsonl(5 rows) is synthetic β hand-written for this server,covering four grammars, both
id_kindvalues, and oneno_citerow. It is a contractexample, not a difficulty example, and the README says so.
The training corpus this reward was developed against is internal and is not included.
Nothing in the server depends on it;
configs/citation_if.yamlpointstrainat agitignored path.
Tests β 327 passing
test_citation_if.pyβ hand-written gate tests, one or more per gate and failure modetest_reward_hack_matrix.pyβ generated matrix, 16 known reward-hack classes x 9 grammarsx 2 ID granularities = 265 cells; pass bar is 100%
test_residual_fuzz.pyβ 3 property tests x 1,000 mutations, both directions: leakmutations must score 0, benign ones (whitespace, punctuation, unicode quotes) must stay 1.
An over-strict gate is as much a regression as a leaky one and much harder to notice.
test_app.pyβ server layer: Responses-API extraction contract and the/verifyresponseLicensing
Code: Apache 2.0. Dependencies:
nemo_gym(Apache 2.0). No external corpus isredistributed β the shipped example rows are synthetic.
Notes for review
verified: false, per the convention that the flag is maintainer-granted.README.mdchange is generated by theupdate-readme-tablepre-commit hook,not hand-edited.
Checklist
pre-commit run --all-files) (so CI lint/format/copyright pass).git commit -s) (so the DCO check passes).