Repository navigation
feat(indian_banking): add multi-turn Indian retail-banking resources server - #2881
Conversation
|
@krishnpci based on code agent review. Following is the finding: Thanks for this contribution — the determinism machinery (frozen clock, content-hashed ids, per-episode pickle clones bound via contextvars) and the judge design (fail-open, transcript-only, never in strict, bounded share) are genuinely well thought out, and attribution hygiene is exemplary. I cloned the branch at That said, I found a few blockers and three correctness bugs in the reward path (two verified by executing the code), so I'd like to see another revision before merge. BlockersB1. The dataset repository is not accessible — config admits it's a placeholder
Consequences as merged:
Please publish the repo (or ship the data in-tree) and rerun the download path end-to-end before merge. B2.
|
6c79372 to
f0a7252
Compare
Thank you for the thorough review - the executed repros were spot-on, and all three ChangesB1 (dataset access). The dataset now ships fully in-tree under B2 (SEQ_STRICT). The contradiction was ours: the README said "default False" while B3 (db/kb story). One story everywhere now: everything ships in-tree; nothing is Correctness bugsC1. C2. C3. In lenient mode the text of a mixed turn is appended to the customer-facing |
|
Please add a manifest.yaml entry under environments/ Reference: https://docs.nvidia.com/nemo/gym/main/contribute/environments/new-environment/#environment-manifest |
| comm, communicate_frac = _communicate_reward(nl_history, communicate_info) | ||
| components["COMMUNICATE"] = comm | ||
| conv_parts.append((W_COMMUNICATE, communicate_frac)) | ||
| # NL_ASSERTION enters only via dense, never strict — see judge.py for why it's bounded. |
There was a problem hiding this comment.
To understand correctly, NL_ASSERTION can never fail strict success.
For the 13 {DB, NL_ASSERTION} tasks — all abstention scenarios with no gold actions — DB correctly passes a do-nothing agent, so the judge is the only check that can tell a correct refusal-with-explanation from a mute agent. It has no power to. A do-nothing, say-nothing trajectory scores 0.925 with a zero judge and 1.0 when the judge is unavailable, since judge_short is 0 for None.
Ex: esc_legit_transfer_locked_1 is the clearest case: the task requires a hand-off, and never transferring still scores 0.925.
There was a problem hiding this comment.
Essentially, for these kind of tasks, make sure they have some deterministic requirement that can still fail and provide an informative reward signal.
There was a problem hiding this comment.
Fixed on the new head. strict now requires at least one non-empty customer-facing message, so the trajectory you demonstrated no longer passes. Tasks can also declare deterministic requirements in evaluation_criteria: max_tool_calls and require_transfer. esc_legit_transfer_locked_1 now carries require_transfer plus a gold transfer_to_human_agents action, so never transferring fails strict. Regression tests cover all three mechanisms.
|
|
||
|
|
||
| def _transcript(nl_history: list[dict]) -> str: | ||
| """Customer-facing turns only. Tool traffic never reaches the judge.""" |
There was a problem hiding this comment.
_transcript() strips all tool traffic, so no assertion about tool behaviour is observable to the judge. Most such assertions have a deterministic backstop, but two have none:
-
chit_greeting_smalltalk_1 ("Agent makes no tool calls"): read calls do not touch the DB and extra lookups are not charged, so 0, 1, or 3 tool calls all score 1.0 on identical text.
-
esc_legit_transfer_locked_1 ("transfers instead of improvising"): world["transferred"] is assigned in three places and never read by any scoring code. Transferring and not transferring both score 1.0.
There was a problem hiding this comment.
Both cases have a deterministic check now. chit_greeting_smalltalk_1 sets max_tool_calls: 0, so any tool call fails strict regardless of the text. esc_legit_transfer_locked_1 requires the transfer, and world["transferred"] is now read by scoring. You were right that it was write-only before.
| except TypeError as exc: # bad/missing kwargs, surface like the server would | ||
| result = json.dumps({"error_code": "VALIDATION_ERROR", "error_message": str(exc)}) | ||
| error = True | ||
| except Exception as exc: # defensive: never kill the rollout |
There was a problem hiding this comment.
is it ever possible that the tool modifies the db, but leads to a tool exception (due to whatever reason) in the subsequent steps.
It doesn't seem to roll the db back to the original pre-call state?
There was a problem hiding this comment.
For example, set_card_controls can apply a valid ATM update, encounter malformed online data, and return error=True while retaining the ATM mutation.
There was a problem hiding this comment.
Correct, this could happen. Fixed at the engine level rather than per tool: apply_tool snapshots the episode db before any write or soft-write tool and restores it when the call reports an error, so the set_card_controls case leaves no trace. Reads skip the snapshot. A regression test covers your exact scenario
| def matches(gold: dict, call: dict) -> bool: | ||
| if gold["name"] != call["name"]: | ||
| return False | ||
| # An errored call satisfies a gold action only if that gold action also |
There was a problem hiding this comment.
Would this make an unexpected failure in the gold trajectory become accepted behavior?
There was a problem hiding this comment.
These two tasks cannot achieve the outcome they require:
- ch3_rd_close_open_fd_1 has ₹92,854.11 available but attempts to book a ₹1,00,000 FD.
- ch5_deposit_restructure_1 has ₹2,02,476.85 available but attempts to book a ₹3,00,000 FD.
Both create_fd calls therefore return INSUFFICIENT_FUNDS. However, because the same failure occurs during gold replay, the scorer treats it as correct and awards strict=1.0 and score=1.0, even though the tasks require successful FD booking by its language.
Please either adjust the balances or FD amounts so booking can succeed, or rewrite these as intentional insufficient-funds tasks. Expected errors should also be marked explicitly rather than inferred from a failing gold replay.
There was a problem hiding this comment.
These two tasks cannot achieve the outcome they require:
ch3_rd_close_open_fd_1has ₹92,854.11 available but attempts to book a ₹1,00,000 FD.ch5_deposit_restructure_1has ₹2,02,476.85 available but attempts to book a ₹3,00,000 FD.
Both create_fd calls therefore return INSUFFICIENT_FUNDS. However, because the same failure occurs during gold replay, the scorer treats it as correct and awardsstrict=1.0andscore=1.0, even though the tasks require successful FD booking.
There was a problem hiding this comment.
Both are fixed. ch3_rd_close_open_fd_1 now books a 75,000 FD against 92,854.11 available after the RD closure payout, and ch5_deposit_restructure_1 books a 36-month 2,00,000 FD against 2,02,476.85 available after the FD948082 closure credits 52,476.85. The amounts are updated consistently in the gold actions, known_info and task_instructions, both bookings are expected to succeed rather than error, and expected errors elsewhere are now marked explicitly with expect_error: true instead of being inferred from the replay. Both tasks replay at exactly 1.0, and ch5 also passed a live end-to-end run.
| indian_banking: | ||
| resources_servers: | ||
| indian_banking: | ||
| entrypoint: app.py |
There was a problem hiding this comment.
good to add allowed_agents: [gymnasium_agent]
This resources server only supports the Gymnasium /reset and /step protocol; its inherited /verify method raises NotImplementedError. Without allowed_agents, Gym treats it as compatible with any agent harness, so it can be paired with simple_agent or another unsupported agent and fail only at runtime. Please add allowed_agents: [gymnasium_agent] so incompatible pairings fail during configuration.
Example
indian_banking:
resources_servers:
indian_banking:
allowed_agents: [gymnasium_agent]
entrypoint: app.py
domain: agent
There was a problem hiding this comment.
Added in environments/indian_banking/ with the config and task_data.py schema.
…server Deterministic banking simulator with 33 tools, an LLM user simulator, a keyword knowledge base, and a partial-credit reward (action/db/communicate + LLM-judged NL assertions). 300 synthetic tasks (250 train / 50 validation) over 197 synthetic customers; all organisation names are invented. Vendored tau2-bench utilities (MIT) attributed in-file and in ATTRIBUTIONS.md. Signed-off-by: krishnpci <323138948+krishnpci@users.noreply.github.com>
…ataset
Problem: review findings on the initial submission. Errored tool calls could
satisfy gold actions and never counted as wrong writes; initialization_actions
leaked into the ACTION/purity checks; lenient-mode mixed turns dropped their
text from the customer-facing history; the judge verdict cache was unbounded
and ignored the judge model; the train/validation splits needed an external
download.
Change:
- ACTION check: an errored call matches a gold action only when the gold
action itself errors on replay; errored writes are excluded from wrong
writes (C1)
- reset(): world state cleared after initialization_actions; init failures
raise instead of seeding a broken world (C2)
- step(): lenient-mode mixed turns keep their text in the customer-facing
history for the simulator, COMMUNICATE check and judge (C3)
- judge: bounded LRU verdict cache keyed by judge model identity
- args_match: numeric coercion ('50000' == 50000), recursing into lists
- data: train/validation/db/kb ship in-tree; no download required (B1/B3)
- docs: SEQ_STRICT documented as shipped True on every reported number (B2);
soft-write list corrected; search_knowledge_base top_k=1 noted in the
agent instruction and every embedded system prompt
- tests: one regression test per fix (21 total)
Signed-off-by: krishnpci <323138948+krishnpci@users.noreply.github.com>
|
Another potential scoring issue: action comparator uses three incompatible multiplicity semantics (core/reward.py) _score_actions computes three different match signals over the same gold/call lists, and they disagree on whether a repeated gold action counts once or twice:
67 of 300 tasks legitimately repeat a gold action (a read-after-write verification step). For these tasks:
|
…manifest Problem: second review round. Do-nothing agents could pass tasks whose only checks were a trivially-satisfied DB hash and a judge that never gates strict; two assertions (no tool calls, must transfer) had no deterministic backstop; an errored multi-write tool call could retain a partial DB mutation; two tasks demanded an FD their balances could not fund, and the gold-error acceptance that legitimised them was inferred rather than declared; the server shipped no task_data schema, no environments/ manifest, and no allowed_agents guard. Change: - strict now requires a non-empty customer-facing assistant message; per-task evaluation_criteria gain max_tool_calls (chit_greeting_smalltalk_1: 0) and require_transfer read from the engine transfer flag (esc_legit_transfer_locked_1, which also gains a gold transfer action) - expected gold errors are explicit: expect_error on the gold action (wi_nonexistent_sr_1); never inferred from replay - ch3_rd_close_open_fd_1 books a 75,000 FD, ch5_deposit_restructure_1 a 200,000 FD - both feasible; gold replay 300/300 enforced by a shipped-data integrity test - engine.apply_tool snapshots the episode DB for write tools and rolls back when the call errors (no partial mutations) - configs: allowed_agents: [gymnasium_agent]; new task_data.py schema; environments/indian_banking/ manifest.yaml + config.yaml (validated by gym env validate, profile custom-gym-agent-loop) - rebased onto current main; tau2-bench row moved into the generated ATTRIBUTIONS.md vendored-components table - tests: 26 in-server (integrity replay, floors, rollback); repo task_data and manifest suites pass Signed-off-by: krishnpci <323138948+krishnpci@users.noreply.github.com>
Problem: the action comparator used three incompatible multiplicity semantics. action_frac and write purity were set-based (one call could satisfy a repeated gold action), seq_frac was a multiplicity-strict LCS, and _gold_order_violated never consumed matches - it mapped both occurrences of a repeated gold action to the same earliest call. 67 of the 300 tasks legitimately repeat a gold action as a read-after-write verification step; on every one of them a byte-perfect trajectory was falsely flagged out of order and charged a duplicate call, capping the reward at 0.955, while an agent that skipped the repeat kept action_frac 1.0 yet failed strict. Change: one shared consumption matcher (_match_gold_calls). Each gold action, in canonical order, consumes the first unconsumed matching call: matched counts consumed golds, order is violated exactly when the in-order match count (LCS) is below matched, a consumed repeat is never an efficiency duplicate, and a non-errored write no gold consumed is a wrong write. A byte-perfect gold replay now scores exactly 1.0 - the shipped-data integrity test asserts this for all 300 tasks - and skipping a mandated repeat lowers action_frac and seq_frac together. Four regression tests cover the repeat, skip, unmandated-duplicate and out-of-order cases. Signed-off-by: krishnpci <323138948+krishnpci@users.noreply.github.com>
Two judge-visible assertions carried internal authoring notes naming the
scenario pattern and its author ("(navya DEP renewal pattern)",
"(navya block+SR pattern)"). Provenance notes are not assertions; removed.
No gold, prompt, or scoring change.
Signed-off-by: krishnpci <323138948+krishnpci@users.noreply.github.com>
The five example_rollouts.jsonl episodes were recorded before the review fixes changed scoring (deterministic floors, consumption-based multiplicity); re-collected them with gym eval run so the shipped artifact matches the shipped reward code (rewards 0.925-1.0, tool calls present in every episode), and regenerated example_metrics.json. Signed-off-by: krishnpci <323138948+krishnpci@users.noreply.github.com>
Problem: the consumption matcher assigned greedily in gold declaration order. With overlapping argument specs - one task pairs a get_deposit_closure_quote gold carrying compare_args: [] (matches any arguments) with a fully-specified repeat - a loose gold could steal the strict gold's call when the agent made both correct calls in reversed order, under-crediting action_frac (0.5 instead of 1.0). Change: _match_gold_calls now computes a maximum bipartite assignment (Kuhn's augmenting paths), so the matched count is independent of gold declaration order, matched >= lcs holds by construction (the LCS is itself a valid assignment), and out-of-order keeps meaning "matched but not realisable in order". Regression test covers the loose+strict overlap in both call orders; the 300-task replay integrity check still requires score == 1.0. Signed-off-by: krishnpci <323138948+krishnpci@users.noreply.github.com>
…rios A generation artifact left the same "Savings SBx, current CAy." sentence twice in known_info on 14 tasks. User-simulator-visible text only; gold actions, agent prompt and scoring unchanged. Signed-off-by: krishnpci <323138948+krishnpci@users.noreply.github.com>
Document that nl_assertions / communicate_info may be present on tasks whose basis does not include the corresponding check: the fields then describe the expected outcome without entering strict or dense, and the judge runs only when NL_ASSERTION is in the basis. No behaviour change. Signed-off-by: krishnpci <323138948+krishnpci@users.noreply.github.com>
f0a7252 to
fe7156c
Compare
@shashank3959 thank you for the careful review, these were valuable catches and each one is addressed on the new head. The branch is also rebased onto current main. On the reward side: strict now requires at least one non-empty customer-facing message, which closes the 0.925 case you demonstrated on the abstention tasks. Tasks can additionally declare max_tool_calls (chit_greeting_smalltalk_1 uses 0) and require_transfer (esc_legit_transfer_locked_1, which also carries a gold transfer_to_human_agents action), and world["transferred"] is now read by scoring as you suggested. The action comparator uses a single matcher for action_frac, ordering and duplicate detection: each gold action is assigned its own call through a maximum bipartite matching, so a trajectory that reproduces gold exactly scores exactly 1.0, the false out-of-order and duplicate charges on the 67 repeat-gold tasks are gone, and skipping a required repeat lowers action_frac and seq_frac consistently. On the engine and data side: tool writes are transactional now, with the episode db snapshotted before write tools and restored when a call reports an error, so the set_card_controls partial-update case leaves no trace. Both FD tasks are feasible (ch3 books 75,000 and ch5 books 2,00,000, comfortably within the balances once the closure proceeds land), and expected errors are declared rather than inferred: an errored call matches a gold action only when it carries expect_error: true, used only by wi_nonexistent_sr_1. A new shipped-data test replays all 300 tasks in CI and fails if a gold call errors without the marking, a marked call does not error, or any replay scores below exactly 1.0. The example rollouts were re-collected under the current scorer so the shipped artifacts match the shipped reward code. Structurally, allowed_agents: [gymnasium_agent] is added to the config as suggested, and there is now an environments/indian_banking/ entry with manifest.yaml and config.yaml (gym env validate passes with the declared profile matching the inferred custom-gym-agent-loop) plus a task_data.py schema, so the repo drift test validates every committed row. Validation on this head: 31 server tests including the 300-task replay check, the task_data and manifest suites, gym env test, and pre-commit --all-files are all green, and every commit is DCO-signed. Happy to adjust anything further. |
|
@krishnpci - Is it possible to move the data (~8.8MB blob) to huggingface and download it, instead of storing it in tree? I think, the B1 blocker comment reported here #2881 (comment) was meant more to ensure the data is possible to download / made public |
|
@shashank3959 , these datasets are entirely synthetic, I have made few changes it should work, without affecting the functionality, will put in huggingface if we still face issues. Can this be triggered again. |
There was a problem hiding this comment.
This is a shared repo-wide file, and we'd prefer it were not updated to pass the CI actually.
From what I understand, the initial commits already had the data in Hugging Face, and if made public it would resolve this?
There was a problem hiding this comment.
yeah, I understood, let me see how to open the huggingface one as it might need some approvals, I have reverted to the approved head.
dae8e1d to
fe7156c
Compare
Per maintainer preference, the four data artifacts leave the tree and the shared secrets baseline stays untouched: train.jsonl (250 rows), validation.jsonl (50), db.json (197 customers) and kb.json (59 articles) are hosted on the NPCI/nemo-gym-indian-banking dataset repo (Apache 2.0) and are byte-identical to the reviewed revision. example.jsonl, the dataset metrics, policy and agent instruction remain in tree. - configs: source blocks restored for the train/validation datasets, so gym dataset collate --download fetches the splits; metrics regenerated with the huggingface identifiers - README: data section documents the download (splits via collate, db/kb via hf download); engine startup error names the dataset repo - data/.gitignore covers the fetched files so local downloads stay untracked - the shipped-data integrity test always validates the committed example.jsonl and includes train/validation whenever they are present locally, skipping cleanly otherwise Verified end to end: download round-trip restores every file byte-identically, 31 server tests and gym env test pass with the data present, the secrets detector passes on the resulting diff with the baseline untouched, and manifest validation passes once the splits are downloaded. Signed-off-by: krishnpci <323138948+krishnpci@users.noreply.github.com>
@shashank3959 Done as you suggested. The dataset artifacts are out of the tree and the shared secrets baseline is untouched: train.jsonl, validation.jsonl, db.json and kb.json now live on the Hugging Face dataset repo NPCI/nemo-gym-indian-banking (Apache 2.0), byte-identical to the reviewed revision, and only example.jsonl plus the metrics stay committed. The configs carry the source blocks again, so gym dataset collate --download fetches the splits, and the README documents the one-time hf download for db.json and kb.json. I verified the full round trip locally: a fresh checkout downloads everything byte-identically, all 31 server tests and gym env test pass, and the secrets detector passes on the resulting diff with no baseline change. The repo is uploaded and is public. The earlier approvals from you was auto-dismissed by the interim pushes, so whenever it suits you this is ready for a re-look, and the CI can be triggered on the new head. |
@shashank3959 all the changes have been done, can you check this once for approval, the CI can be triggered as well on this. |
|
/ok to test 171f827 |
|
@shashank3959 thanks a lot ! |
…server (#2881) feat(indian_banking): add multi-turn Indian retail-banking resources server (#2881) Adds `resources_servers/indian_banking`, a multi-turn customer-support environment where the policy staffs the chat desk of a synthetic India-based retail bank. The agent talks to an LLM-simulated customer and operates 34 tools (33 servicing tools plus human hand-off) against a deterministic in-process banking engine, consulting a written policy and a 59-article keyword knowledge base. Public tau-style environments cover airline, retail and telecom. Retail banking has a different tool surface (deposits, loans, cards, mandates, cheques, servicing requests, hand-off) and, in the Indian setting, its own rules: KYC/re-KYC gating, INR rate cards and EMIs, government schemes, TDS. ### Scoring Each task's `reward_basis` selects the active checks: | Check | Passes when | |---|---| | ACTION | every gold call made in order with matching arguments, matched consumption-consistently (each gold action satisfied at most once); no unrequested write; a gold call may only error when marked `expect_error` | | DB | final database hash equals a gold-action replay after state normalisation | | COMMUNICATE | every required fact told to the customer; no internal IDs or tool names leaked | | NL_ASSERTION | LLM judge over the customer-facing transcript only; never in the strict gate; fails open | `strict` is the AND of the active binary checks. `reward` is `1 - efficiency_cost` on a pass, else `strict + 0.4·dense`, so partial credit stays below the smallest pass increment. An optional `require_transfer` criterion asserts a human hand-off actually occurred. ### Implementation `app.py` is a `GymnasiumServer` with per-episode database isolation under a frozen clock and ID seed, `contextvars`-bound so concurrent rollouts stay isolated. Tool writes are transactional. No changes to NeMo Gym core; runtime dependency `nemo_gym` only. Two evaluator utilities derive from tau2-bench (MIT, attributed in-file and in `ATTRIBUTIONS.md`). ### Data `train.jsonl` (250 rows), `validation.jsonl` (50), `db.json` (197 synthetic customers) and `kb.json` (59 articles) are hosted on the public Hugging Face dataset repo `NPCI/nemo-gym-indian-banking` (Apache 2.0), fetched by `gym dataset collate --download`. Only `example.jsonl`, metrics, policy and agent instruction are committed. All data is synthetic. 31 offline tests; `verified: false` pending maintainer baselining. Signed-off-by: krishnpci <323138948+krishnpci@users.noreply.github.com> Signed-off-by: Junkeun Yi <jkyi@nvidia.com>
What does this PR do?
Resolves #2880
Adds
resources_servers/indian_banking, a multi-turn customer-support environment in which the policy staffs the chat desk of a synthetic India-based retail bank: the agent talks to an LLM-simulated customer, operates 34 tools (33 servicing tools plus human hand-off) against a deterministic in-process banking engine, consults a written policy and a 59-article keyword knowledge base, and is scored on the tool calls it made, the database it left behind, and what it told the customer.Public tau-style environments cover airline, retail and telecom; retail banking has a different tool surface (deposits, loans, cards, mandates, cheques, servicing requests, hand-off) and, in the Indian setting, its own rules (KYC/re-KYC gating, INR rate cards and EMIs, government schemes, TDS). The reward is built for training as well as evaluation: a strict gate for benchmarking and a bounded dense signal with an LLM-judged NL-assertion component that gives GRPO-style training a gradient without letting partial credit reach a pass.
Ships fully in-tree —
train.jsonl(250 rows),validation.jsonl(50),db.json(197 synthetic customers),kb.json(59 articles), policy, agent instruction, 5 example rows / metrics / rollouts — no external download.app.pyis aGymnasiumServerwith per-episode database isolation under a frozen clock and ID seed,contextvars-bound so concurrent rollouts stay isolated. 21 offline tests. No changes to NeMo Gym core; runtime dependencynemo_gymonly. Two small evaluator utilities derive from tau2-bench (MIT, attributed in-file and inATTRIBUTIONS.md).Verification logic
Each task's
reward_basisselects the checks:SEQ_STRICT = True, documented) with matching arguments; no unrequested write; an errored call counts only if the gold action itself errors on replaystrict= AND of the active binary checks;reward=1 − efficiency_coston a pass, elsestrict + 0.4·dense, so partial credit stays below the smallest pass increment. Every one of the 300 tasks replays its gold actions atstrict = 1.0.Review feedback addressed (second commit)
Problem: errored tool calls could satisfy gold actions and never counted as wrong writes;
initialization_actionsleaked into the ACTION/purity checks; lenient-mode mixed turns dropped their text from the customer-facing history; the judge cache was unbounded and ignored the judge model; the splits needed an external download.Change: gold-error-aware ACTION matching; world state cleared after init (failures raise); mixed-turn text kept in the history; bounded LRU judge cache keyed by judge model; numeric argument coercion; dataset moved in-tree; one regression test per fix.
Validation
pytest resources_servers/indian_banking/tests— 21 passed;gym env test --resources-server indian_bankingpasses;pre-commit run --all-filesclean.example_metrics.jsonreports 5 examples;example_rollouts.jsonlholds exactly 5 rollouts (rewards 0.15–1.0).main(b8250f3d); both commits DCO-signed.Notes for reviewers
env.yaml); the judge must be a separate model server from the policy.README.mdchange is generated by theupdate-readme-tablepre-commit hook, not hand-edited.verified: falseas injected; awaits maintainer baselining.Checklist
pre-commit run --all-files) (so CI lint/format/copyright pass).git commit -s) (so the DCO check passes).