Skip to content

Commit 42e1ed2

Browse files
committed
fix(data): preserve and override conversation system prompts
Signed-off-by: Piotr Żelasko <pzelasko@nvidia.com>
1 parent 0572227 commit 42e1ed2

5 files changed

Lines changed: 258 additions & 47 deletions

File tree

docs/source/dataloaders.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -627,6 +627,14 @@ Dataloading configuration example::
627627
- type: multimodal_conversation
628628
manifest_filepath: /path/to/chat_{0..N}.jsonl
629629
audio_locator_tag: [audio]
630+
tags:
631+
system_prompt: You are a helpful assistant.
632+
override_system_prompt: true
633+
634+
``system_prompt`` is inserted when the source conversation has no system turn.
635+
By default, a system prompt already present in the data takes precedence. Set
636+
``override_system_prompt: true`` to remove data-provided system turns and use
637+
the configured prompt instead.
630638

631639
Python object example::
632640

docs/source/speechlm2/datasets.rst

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,24 @@ the index tools. Source JSONL and tar files are immutable; ``.idx``,
220220
For stateful training, put ``index_pack`` on each outer dataset entry and keep
221221
``indexed: true`` and ``use_stateful_dataloader: true`` on the training dataset.
222222

223+
System prompt policy
224+
^^^^^^^^^^^^^^^^^^^^
225+
226+
ShareGPT turns whose ``from`` field is ``system`` remain system turns. The
227+
``share_gpt``, ``share_gpt_webdataset``, and ``multimodal_conversation`` readers
228+
also accept a configured prompt through ``tags``:
229+
230+
.. code-block:: yaml
231+
232+
tags:
233+
system_prompt: "You are a helpful assistant."
234+
override_system_prompt: true
235+
236+
Without ``override_system_prompt``, a system prompt already present in the data
237+
is preserved and the configured prompt is inserted only when one is absent.
238+
When the override is enabled, the configured prompt replaces all system turns
239+
from the data.
240+
223241
Audio fields and placeholder binding
224242
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
225243

nemo/collections/common/data/lhotse/cutset.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,7 @@ def read_multimodal_conversation_jsonl(config: DictConfig) -> tuple[CutSet, bool
471471
shuffle_shards=config.shuffle,
472472
shard_seed=config.shard_seed,
473473
system_prompt=config.get("tags", {}).get("system_prompt"),
474+
override_system_prompt=config.get("tags", {}).get("override_system_prompt", False),
474475
context=config.get("tags", {}).get("context"),
475476
slice_length=config.get("slice_length"),
476477
indexed=config.get("indexed", False),
@@ -500,6 +501,8 @@ def read_share_gpt_as_conversation(config) -> tuple[CutSet, bool]:
500501
audio_root=config.get("audio_root"),
501502
audio_path_prefix_map=config.get("audio_path_prefix_map"),
502503
token_equivalent_duration=config.get("token_equivalent_duration"),
504+
system_prompt=config.get("tags", {}).get("system_prompt"),
505+
override_system_prompt=config.get("tags", {}).get("override_system_prompt", False),
503506
shuffle_shards=config.shuffle,
504507
shard_seed=config.shard_seed,
505508
slice_length=config.get("slice_length"),
@@ -528,6 +531,8 @@ def read_share_gpt_webdataset_as_conversation(config) -> tuple[CutSet, bool]:
528531
audio_locator_tag=config.audio_locator_tag,
529532
audio_placeholders=config.get("audio_placeholders"),
530533
token_equivalent_duration=config.get("token_equivalent_duration"),
534+
system_prompt=config.get("tags", {}).get("system_prompt"),
535+
override_system_prompt=config.get("tags", {}).get("override_system_prompt", False),
531536
shuffle_shards=config.shuffle,
532537
shard_seed=config.shard_seed,
533538
indexed=config.get("indexed", False),

nemo/collections/common/data/lhotse/text_adapters.py

Lines changed: 68 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -412,7 +412,7 @@ class NeMoSFTJsonlAdapter(IteratorNode):
412412
"conversations": [
413413
{
414414
"value": str,
415-
"from": "User" | "Assistant",
415+
"from": "System" | "User" | "Assistant",
416416
"canonical_form": str,
417417
"label": str | null
418418
},
@@ -1389,6 +1389,7 @@ class NeMoMultimodalConversationJsonlAdapter(IteratorNode):
13891389
index_pack_max_open_files: int = 32
13901390
skip_missing_manifest_entries: bool = False
13911391
fault_tolerant_audio_loading: bool = True
1392+
override_system_prompt: bool = False
13921393

13931394
def __post_init__(self):
13941395
raw_manifest_filepath = self.manifest_filepath
@@ -1543,8 +1544,7 @@ def _build_conversation_local(self, data: dict, manifest_path: str) -> NeMoMulti
15431544
raise RuntimeError(message) from ex
15441545
if self.context is not None and turns[0].role == "user" and isinstance(turns[0], AudioTurn):
15451546
turns = [TextTurn(role="user", value=self.context)] + turns
1546-
if self.system_prompt is not None and turns[0].role != "system":
1547-
turns = [TextTurn(role="system", value=self.system_prompt)] + turns
1547+
turns = _apply_system_prompt(turns, self.system_prompt, self.override_system_prompt)
15481548
return NeMoMultimodalConversation(
15491549
id=data["id"],
15501550
turns=turns,
@@ -1626,8 +1626,7 @@ def _build_conversation_tarred(self, data: dict, tar_reader, tar_path: str) -> N
16261626
]
16271627
if self.context is not None and turns[0].role == "user" and isinstance(turns[0], AudioTurn):
16281628
turns = [TextTurn(role="user", value=self.context)] + turns
1629-
if self.system_prompt is not None and turns[0].role != "system":
1630-
turns = [TextTurn(role="system", value=self.system_prompt)] + turns
1629+
turns = _apply_system_prompt(turns, self.system_prompt, self.override_system_prompt)
16311630
return NeMoMultimodalConversation(
16321631
id=data["id"],
16331632
turns=turns,
@@ -1764,8 +1763,7 @@ def _iter_tar(self):
17641763
]
17651764
if self.context is not None and turns[0].role == "user" and isinstance(turns[0], AudioTurn):
17661765
turns = [TextTurn(role="user", value=self.context)] + turns
1767-
if self.system_prompt is not None and turns[0].role != "system":
1768-
turns = [TextTurn(role="system", value=self.system_prompt)] + turns
1766+
turns = _apply_system_prompt(turns, self.system_prompt, self.override_system_prompt)
17691767
yield NeMoMultimodalConversation(
17701768
id=data["id"],
17711769
turns=turns,
@@ -1818,7 +1816,7 @@ class NeMoMultimodalConversationShareGPTJsonlAdapter(IteratorNode):
18181816
"conversations": [
18191817
{
18201818
"value": str, # text message, may contain <sound> or <speech> placeholder
1821-
"from": "human" | "gpt",
1819+
"from": "system" | "human" | "gpt",
18221820
},
18231821
...
18241822
],
@@ -1850,6 +1848,8 @@ class NeMoMultimodalConversationShareGPTJsonlAdapter(IteratorNode):
18501848
excluded_manifest_lines: Sequence[int] | None = None
18511849
excluded_manifest_lines_sha256: str | None = None
18521850
approved_exclusion_audit_sha256: str | None = None
1851+
system_prompt: str | None = None
1852+
override_system_prompt: bool = False
18531853

18541854
def __post_init__(self):
18551855
raw_manifest_filepath = self.manifest_filepath
@@ -2182,6 +2182,7 @@ def resolve_collection_cut(turn):
21822182
conversations,
21832183
resolve_collection_cut,
21842184
)
2185+
turns = _apply_system_prompt(turns, self.system_prompt, self.override_system_prompt)
21852186
if used_route_indexes != set(range(len(routes))):
21862187
raise ValueError(
21872188
f"ShareGPT route row {route_row_idx} contains unused records: "
@@ -2195,24 +2196,26 @@ def resolve_collection_cut(turn):
21952196
if self._tar_readers:
21962197
tar_reader = self._tar_readers[shard_idx]
21972198
tar_path = self.tarred_audio_filepaths[shard_idx]
2199+
turns = _ShareGPTConversationParser.create_turns(
2200+
self.audio_locator_tag,
2201+
conversations,
2202+
lambda t: self._resolve_cut_from_indexed_tar(t, tar_reader, tar_path),
2203+
)
21982204
return NeMoMultimodalConversation(
21992205
id=data.get("id", "missing-example-id"),
2200-
turns=_ShareGPTConversationParser.create_turns(
2201-
self.audio_locator_tag,
2202-
conversations,
2203-
lambda t: self._resolve_cut_from_indexed_tar(t, tar_reader, tar_path),
2204-
),
2206+
turns=_apply_system_prompt(turns, self.system_prompt, self.override_system_prompt),
22052207
token_equivalent_duration=self.token_equivalent_duration,
22062208
)
22072209
if manifest_path is None:
22082210
manifest_path = self._cuts_readers[shard_idx].path
2211+
turns = _ShareGPTConversationParser.create_turns(
2212+
self.audio_locator_tag,
2213+
conversations,
2214+
lambda t, _p=manifest_path: self._resolve_cut_from_path(t, _p),
2215+
)
22092216
return NeMoMultimodalConversation(
22102217
id=data.get("id", "missing-example-id"),
2211-
turns=_ShareGPTConversationParser.create_turns(
2212-
self.audio_locator_tag,
2213-
conversations,
2214-
lambda t, _p=manifest_path: self._resolve_cut_from_path(t, _p),
2215-
),
2218+
turns=_apply_system_prompt(turns, self.system_prompt, self.override_system_prompt),
22162219
token_equivalent_duration=self.token_equivalent_duration,
22172220
)
22182221
except _SHAREGPT_AUDIO_LOADING_ERRORS as e:
@@ -2441,11 +2444,12 @@ def _iter_tar(self):
24412444
elif cntr == self.slice_length:
24422445
break
24432446

2447+
turns = _ShareGPTConversationParser.create_turns(
2448+
self.audio_locator_tag, conversations, lambda t: cuts.popleft()
2449+
)
24442450
yield NeMoMultimodalConversation(
24452451
id=data.get("id", "missing-example-id"),
2446-
turns=_ShareGPTConversationParser.create_turns(
2447-
self.audio_locator_tag, conversations, lambda t: cuts.popleft()
2448-
),
2452+
turns=_apply_system_prompt(turns, self.system_prompt, self.override_system_prompt),
24492453
token_equivalent_duration=self.token_equivalent_duration,
24502454
)
24512455
cntr += 1
@@ -2474,13 +2478,14 @@ def _iter_jsonl(self):
24742478
for data in jsonl_iter:
24752479
try:
24762480
conversations = _ShareGPTConversationParser(self.audio_placeholders, data).transform()
2481+
turns = _ShareGPTConversationParser.create_turns(
2482+
self.audio_locator_tag,
2483+
conversations,
2484+
lambda t, _p=path: self._resolve_cut_from_path(t, _p),
2485+
)
24772486
yield NeMoMultimodalConversation(
24782487
id=data.get("id", "missing-example-id"),
2479-
turns=_ShareGPTConversationParser.create_turns(
2480-
self.audio_locator_tag,
2481-
conversations,
2482-
lambda t, _p=path: self._resolve_cut_from_path(t, _p),
2483-
),
2488+
turns=_apply_system_prompt(turns, self.system_prompt, self.override_system_prompt),
24842489
token_equivalent_duration=self.token_equivalent_duration,
24852490
)
24862491
except _SHAREGPT_AUDIO_LOADING_ERRORS as e:
@@ -2537,6 +2542,8 @@ class NeMoMultimodalConversationShareGPTWebdatasetAdapter(IteratorNode):
25372542
index_pack_max_open_files: int = 32
25382543
skip_missing_manifest_entries: bool = False
25392544
fault_tolerant_audio_loading: bool = True
2545+
system_prompt: str | None = None
2546+
override_system_prompt: bool = False
25402547

25412548
def __post_init__(self):
25422549
if self.wds_sample_index_version not in (1, 2):
@@ -2683,13 +2690,14 @@ def _yield_from_sample(self, json_data, audio_bytes, audio_name):
26832690
conversations = _ShareGPTConversationParser(self.audio_placeholders, json_data, audio_name).transform()
26842691
recording = _sharegpt_recording_from_bytes(audio_bytes, recording_id=sample_id)
26852692
base_cut = recording.to_cut()
2693+
turns = _ShareGPTConversationParser.create_turns(
2694+
self.audio_locator_tag,
2695+
conversations,
2696+
lambda t: base_cut.truncate(offset=t.get("offset", 0.0), duration=t.get("duration")),
2697+
)
26862698
return NeMoMultimodalConversation(
26872699
id=json_data.get("id", sample_id),
2688-
turns=_ShareGPTConversationParser.create_turns(
2689-
self.audio_locator_tag,
2690-
conversations,
2691-
lambda t: base_cut.truncate(offset=t.get("offset", 0.0), duration=t.get("duration")),
2692-
),
2700+
turns=_apply_system_prompt(turns, self.system_prompt, self.override_system_prompt),
26932701
token_equivalent_duration=self.token_equivalent_duration,
26942702
)
26952703

@@ -2760,13 +2768,14 @@ def resolve_cut(turn):
27602768
decoded_cuts[member.name] = cut
27612769
return cut.truncate(offset=turn.get("offset", 0.0), duration=turn.get("duration"))
27622770

2771+
turns = _ShareGPTConversationParser.create_turns(
2772+
self.audio_locator_tag,
2773+
conversations,
2774+
resolve_cut,
2775+
)
27632776
return NeMoMultimodalConversation(
27642777
id=bundle.json_data.get("id", bundle.sample_key),
2765-
turns=_ShareGPTConversationParser.create_turns(
2766-
self.audio_locator_tag,
2767-
conversations,
2768-
resolve_cut,
2769-
),
2778+
turns=_apply_system_prompt(turns, self.system_prompt, self.override_system_prompt),
27702779
token_equivalent_duration=self.token_equivalent_duration,
27712780
)
27722781

@@ -2999,6 +3008,23 @@ def _normalize_audio_placeholders(val: Union[str, list[str], None]) -> list[str]
29993008
return [val] if isinstance(val, str) else list(val)
30003009

30013010

3011+
def _apply_system_prompt(
3012+
turns: list[TextTurn | AudioTurn],
3013+
system_prompt: str | None,
3014+
override_system_prompt: bool,
3015+
) -> list[TextTurn | AudioTurn]:
3016+
"""Apply a configured system prompt without changing the default data-first policy."""
3017+
if system_prompt is None:
3018+
return turns
3019+
3020+
configured_turn = TextTurn(role="system", value=system_prompt)
3021+
if override_system_prompt:
3022+
return [configured_turn] + [turn for turn in turns if turn.role != "system"]
3023+
if turns and turns[0].role == "system":
3024+
return turns
3025+
return [configured_turn] + turns
3026+
3027+
30023028
class _ShareGPTConversationParser:
30033029
"""Normalize ShareGPT multimodal records for the conversation adapters.
30043030
@@ -3168,7 +3194,12 @@ def count_audio_placeholders(cls, text: str, placeholders: list[str]) -> int:
31683194

31693195
@staticmethod
31703196
def role(turn: dict) -> str:
3171-
return "user" if turn["from"].lower() in ("human", "user") else "assistant"
3197+
role = turn["from"].lower()
3198+
if role in ("human", "user"):
3199+
return "user"
3200+
if role == "system":
3201+
return "system"
3202+
return "assistant"
31723203

31733204
@classmethod
31743205
def turn_can_consume_audio(cls, turn: dict) -> bool:

0 commit comments

Comments
 (0)