-
Notifications
You must be signed in to change notification settings - Fork 485
feat: Add Tau bench environment #2479
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
Open
ashors1
wants to merge
39
commits into
main
Choose a base branch
from
ashors/multiturn-env
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 6 commits
Commits
Show all changes
39 commits
Select commit
Hold shift + click to select a range
b1da6f1
[WIP] Tau bench multi-turn environment
ashors1 1e4a1a4
update dependencies and fix some bugs
ashors1 31129e7
Fixes to tau bench environment
ashors1 78dc0bd
add option to mock user and judge with dummy responses
ashors1 762de3f
remove debug code
ashors1 c0c27ec
terminate mock conversations with some probability
ashors1 3deb212
address comments
ashors1 27f5c64
update uv.lock and add a small assertion
ashors1 4f35191
robustness improvements
ashors1 f32be23
add tau bench recipe that uses local vllm endpoints for user/judge mo…
ashors1 2f3f61b
remove debug code
ashors1 c93b04e
handle prompt grouping in multi-dataset case
ashors1 00c0ca9
add documentation, address some small issues
ashors1 a735457
Merge branch 'main' of github.com:NVIDIA-NeMo/RL into ashors/multitur…
ashors1 bdce2c4
Merge branch 'main' of github.com:NVIDIA-NeMo/RL into ashors/multitur…
ashors1 eb8aa91
fix bug in yaml
ashors1 045e231
fix some bugs
ashors1 7e2545a
lint
ashors1 cee20e7
Merge branch 'main' of github.com:NVIDIA-NeMo/RL into ashors/multitur…
ashors1 223e52f
formatting
ashors1 230cc70
formatting, fix unit test
ashors1 363a63f
Merge branch 'main' of github.com:NVIDIA-NeMo/RL into ashors/multitur…
ashors1 b2d33de
remove all references to extract_initial_prompt_messages
ashors1 2547af8
Merge branch 'main' of github.com:NVIDIA-NeMo/RL into ashors/multitur…
ashors1 9751c42
lint
ashors1 d94788b
Merge branch 'main' of github.com:NVIDIA-NeMo/RL into ashors/multitur…
ashors1 97b9b98
Merge branch 'ashors/multiturn-env' of github.com:NVIDIA-NeMo/RL into…
ashors1 82b27b5
update uv.lock
ashors1 202821a
fix unit tests
ashors1 8b434ac
fix unit tests
ashors1 968b1ff
update test
ashors1 423d4a0
address comments, fix a bug with dynamic sampling
ashors1 5499b99
fix failing tests
ashors1 eab86d3
lint
ashors1 cc319cb
Merge branch 'main' of github.com:NVIDIA-NeMo/RL into ashors/multitur…
ashors1 a370039
Merge branch 'main' of github.com:NVIDIA-NeMo/RL into ashors/multitur…
ashors1 ff60101
fix tests
ashors1 fd0dd6c
fix nemo gym tests
ashors1 09413fe
Merge branch 'main' of github.com:NVIDIA-NeMo/RL into ashors/multitur…
ashors1 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
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,113 @@ | ||
| # GRPO training on tau-bench: multi-turn customer service tasks with LLM-as-judge rewards. | ||
| # | ||
| # The agent must complete realistic retail or airline service tasks by calling domain | ||
| # tools across multiple turns. Rewards blend tau-bench's deterministic task-completion | ||
| # check with an optional LLM judge score for policy compliance. | ||
| # | ||
| # Data preparation: run scripts/prepare_tau_bench_data.py to generate the JSONL files | ||
| # before starting training. The script calls tau-bench's env.reset() for each task | ||
| # so that the prompts (tool definitions, domain wiki, initial user message) are | ||
| # exactly consistent with the environment state. | ||
| # | ||
| # Tool call format expected from the policy: | ||
| # <tool_call>{"name": "tool_name", "arguments": {"key": "value"}}</tool_call> | ||
| # Text without a <tool_call> block is treated as a respond action to the user. | ||
|
|
||
| defaults: "grpo_math_1B_megatron.yaml" | ||
|
|
||
| grpo: | ||
| num_prompts_per_step: 16 | ||
| num_generations_per_prompt: 8 | ||
| # tau-bench episodes can be up to ~50 tool calls; set headroom above max_steps. | ||
| max_rollout_turns: 56 | ||
| max_num_steps: 10000 | ||
| max_num_epochs: 1 | ||
|
|
||
| checkpointing: | ||
| checkpoint_dir: "results/grpo-tau-bench" | ||
| metric_name: "val:tau_bench/task_completion_rate" | ||
| higher_is_better: true | ||
|
|
||
| policy: | ||
| model_name: "Qwen/Qwen2.5-7B-Instruct" | ||
| max_total_sequence_length: 8192 | ||
| train_micro_batch_size: 1 | ||
| logprob_batch_size: 1 | ||
| train_global_batch_size: 128 | ||
|
|
||
| megatron_cfg: | ||
| enabled: true | ||
| tensor_model_parallel_size: 2 | ||
| pipeline_model_parallel_size: 1 | ||
| context_parallel_size: 1 | ||
| pipeline_dtype: ${policy.precision} | ||
|
|
||
| generation: | ||
| backend: "vllm" | ||
| max_new_tokens: 512 # per turn; tool calls are short | ||
| temperature: 1.0 | ||
| top_p: 1.0 | ||
| stop_strings: null # stop strings are supplied per-turn by the environment | ||
| stop_token_ids: null | ||
| vllm_cfg: | ||
| async_engine: false | ||
| tensor_parallel_size: 2 | ||
| gpu_memory_utilization: 0.6 | ||
| max_model_len: ${policy.max_total_sequence_length} | ||
|
|
||
| cluster: | ||
| gpus_per_node: 8 | ||
| num_nodes: 1 | ||
|
|
||
| data: | ||
| train: | ||
| dataset_name: "tau_bench" | ||
| tau_bench_env_name: "retail" # "retail" or "airline" | ||
| split: "train" | ||
| validation: | ||
| dataset_name: "tau_bench" | ||
| tau_bench_env_name: "retail" | ||
| split: "dev" | ||
| default: | ||
| prompt_file: null | ||
| system_prompt_file: null | ||
| processor: "tau_bench_data_processor" | ||
| env_name: "tau_bench" | ||
|
|
||
| env: | ||
| tau_bench: | ||
| num_workers: 2 | ||
| env_name: "retail" # "retail" or "airline" | ||
| task_split: "train" | ||
| user_strategy: "mock" | ||
| # User simulator model: any OpenAI-compatible endpoint. | ||
| user_model: null # unused when user_strategy="mock". Required if user_strategy="llm" | ||
| user_base_url: null # unused when user_strategy="mock". Required if user_strategy="llm" | ||
| user_api_key: null # unused when user_strategy="mock" | ||
| max_steps: 50 | ||
| # LLM-as-judge: scores policy compliance and communication quality. | ||
| # Set judge_model to null to disable and use pure tau-bench rewards. | ||
| judge_model: "mock" | ||
| judge_base_url: null # unused when judge_model="mock" | ||
| judge_api_key: null # unused when judge_model="mock" | ||
| judge_weight: 0.3 # 70% task-completion + 30% judge score | ||
| mock_latency_s: 0.1 # how much time to wait before returning mock user/judge generations | ||
| mock_stop_prob: 0.4 | ||
|
|
||
| logger: | ||
| log_dir: "logs" | ||
| wandb_enabled: false | ||
| tensorboard_enabled: false | ||
| mlflow_enabled: false | ||
| swanlab_enabled: false | ||
| monitor_gpus: true | ||
| wandb: | ||
| project: "ashors-grpo-dev" | ||
| name: "grpo-tau-bench" | ||
| swanlab: | ||
| project: "grpo-dev" | ||
| name: "grpo-tau-bench" | ||
| mlflow: | ||
| experiment_name: "grpo-dev" | ||
| run_name: "grpo-tau-bench" | ||
| tracking_uri: "http://localhost:5000" | ||
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
168 changes: 168 additions & 0 deletions
168
nemo_rl/data/datasets/response_datasets/tau_bench_dataset.py
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,168 @@ | ||
| # Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
| """TauBench dataset for nemo-rl GRPO training. | ||
|
|
||
| Each sample contains: | ||
| - messages: [system_message, initial_user_message] | ||
| - extra_env_info: {task_index, episode_id: null, step_count: 0} | ||
| - task_name: "tau_bench" | ||
|
|
||
| The system message is built from the environment's tool definitions, domain | ||
| wiki, and policy rules, exactly matching what TauBenchEnvironment initialises | ||
| at training time. | ||
| """ | ||
|
|
||
| from typing import Any | ||
|
|
||
| import ray | ||
| from datasets import Dataset | ||
|
|
||
| from nemo_rl.data.datasets.raw_dataset import RawDataset | ||
| from nemo_rl.distributed.virtual_cluster import PY_EXECUTABLES | ||
|
|
||
|
|
||
| _TOOL_CALL_INSTRUCTIONS = """\ | ||
| To call a tool, wrap a JSON object in <tool_call> tags: | ||
|
|
||
| <tool_call>{"name": "tool_name", "arguments": {"param": "value"}}</tool_call> | ||
|
|
||
| Call one tool at a time and wait for the result before proceeding. \ | ||
| To send a final message to the customer (ending the interaction), \ | ||
| respond with plain text only — no <tool_call> tags.""" | ||
|
|
||
|
|
||
| def _format_tool(tool_info: dict[str, Any]) -> str: | ||
| lines = [f"### {tool_info['name']}"] | ||
| if "description" in tool_info: | ||
| lines.append(f"Description: {tool_info['description']}") | ||
| params = tool_info.get("parameters", {}) | ||
| if isinstance(params, dict): | ||
| props = params.get("properties", {}) | ||
| required = set(params.get("required", [])) | ||
| if props: | ||
| lines.append("Parameters:") | ||
| for name, schema in props.items(): | ||
| req = " (required)" if name in required else " (optional)" | ||
| ptype = schema.get("type", "any") | ||
| desc = schema.get("description", "") | ||
| lines.append(f" - {name} ({ptype}{req}): {desc}") | ||
| return "\n".join(lines) | ||
|
|
||
|
|
||
| def _build_system_prompt(env: Any, env_name: str) -> str: | ||
| tool_infos = [] | ||
| for t in env.tools_info or []: | ||
| # tau_bench wraps each tool spec as {"type": "function", "function": {...}} | ||
| info = t["function"] if "function" in t else t | ||
| tool_infos.append(info) | ||
|
|
||
| tools_section = "\n\n".join(_format_tool(t) for t in tool_infos) | ||
| rules_section = "\n".join(f"- {r}" for r in (env.rules or [])) | ||
| wiki = (env.wiki or "").strip() | ||
|
|
||
| return "\n\n".join( | ||
| filter( | ||
| None, | ||
| [ | ||
| f"You are a customer service agent for a {env_name} company. " | ||
| "Complete the customer's request using the tools below.", | ||
| f"## Tool Usage\n\n{_TOOL_CALL_INSTRUCTIONS}", | ||
| f"## Available Tools\n\n{tools_section}" if tools_section else None, | ||
| f"## Domain Knowledge\n\n{wiki}" if wiki else None, | ||
| f"## Policies\n\n{rules_section}" if rules_section else None, | ||
| ], | ||
| ) | ||
| ) | ||
|
|
||
|
|
||
| def _build_records(tau_bench_env_name: str, split: str) -> list[dict[str, Any]]: | ||
| """Build the list of dataset records from a tau-bench environment. | ||
|
|
||
| This function is invoked via Ray remote with PY_EXECUTABLES.TAU_BENCH so | ||
| that tau_bench is only required in the TAU_BENCH Ray environment, not in | ||
| the driver process. | ||
| """ | ||
| from tau_bench.envs import get_env | ||
|
|
||
| # Use the "human" strategy so no LLM provider is required at data-load | ||
| # time. We read task.instruction directly — env.reset() is intentionally | ||
| # NOT called here because HumanUserSimulationEnv.reset() blocks on | ||
| # input(), which would hang the process for every task. | ||
| env = get_env( | ||
| env_name=tau_bench_env_name, | ||
| user_strategy="human", | ||
| user_model="", | ||
| task_split=split, | ||
| ) | ||
|
|
||
| system_prompt = _build_system_prompt(env, tau_bench_env_name) | ||
|
|
||
| # The actual customer opening message comes from env.reset() (via the LLM | ||
| # user simulator) at training time. TauBenchWorker.execute() returns it as | ||
| # the first observation (a "pre-step") before any agent action is processed. | ||
| # The placeholder below just gives the model a neutral starting point for | ||
| # the wasted generation on that pre-step turn; the real conversation begins | ||
| # once the agent sees the customer's actual first message. | ||
| placeholder_user_msg = "Hi! How can I help you today?" | ||
| return [ | ||
| { | ||
| "messages": [ | ||
| {"role": "system", "content": system_prompt}, | ||
| {"role": "user", "content": placeholder_user_msg}, | ||
| ], | ||
| "extra_env_info": { | ||
| "task_index": task_index, | ||
| "episode_id": None, | ||
| "step_count": 0, | ||
| }, | ||
| "task_name": "tau_bench", | ||
| } | ||
| for task_index, task in enumerate(env.tasks) | ||
| ] | ||
|
|
||
|
|
||
| class TauBenchDataset(RawDataset): | ||
| """Dataset built from a tau-bench environment. | ||
|
|
||
| Calls tau_bench.envs.get_env() at construction time and iterates every | ||
| task in the requested split to produce (system_prompt, initial_user_message) | ||
| pairs. No offline data-preparation step is needed. | ||
|
|
||
| Args: | ||
| tau_bench_env_name: tau-bench domain — "retail" or "airline". | ||
| split: Dataset split — "train", "dev", or "test" (default: "train"). | ||
| repeat: Number of times to repeat the dataset (default: 1). | ||
| """ | ||
|
|
||
| def __init__( | ||
| self, | ||
| tau_bench_env_name: str, | ||
| split: str = "train", | ||
| repeat: int = 1, | ||
| **kwargs: Any, | ||
| ) -> None: | ||
| self.task_name = "tau_bench" | ||
|
|
||
| # Run record construction in the TAU_BENCH Ray environment so that | ||
| # tau_bench is not required in the driver process. | ||
| records = ray.get( | ||
| ray.remote(_build_records) | ||
| .options(runtime_env={"py_executable": PY_EXECUTABLES.TAU_BENCH}) | ||
| .remote(tau_bench_env_name, split) | ||
| ) | ||
|
|
||
| self.dataset = Dataset.from_list(records) | ||
|
|
||
| if repeat > 1: | ||
| self.dataset = self.dataset.repeat(repeat) |
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
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
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
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.