renderers 0.1.8.dev53__tar.gz → 0.1.8.dev55__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/PKG-INFO +1 -1
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/docs/renderer-config.md +2 -1
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/renderers/__init__.py +4 -0
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/renderers/_version.py +2 -2
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/renderers/base.py +30 -3
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/renderers/configs.py +22 -0
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/renderers/laguna_xs2.py +398 -18
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/renderers/parsing.py +27 -16
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/tests/conftest.py +3 -0
- renderers-0.1.8.dev55/tests/test_laguna_xs21.py +376 -0
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/tests/test_renderer_config_parity.py +1 -0
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/tests/test_roundtrip.py +4 -0
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/.github/workflows/publish-dev.yml +0 -0
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/.github/workflows/publish.yml +0 -0
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/.github/workflows/style.yml +0 -0
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/.github/workflows/test.yml +0 -0
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/.gitignore +0 -0
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/.pre-commit-config.yaml +0 -0
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/LICENSE +0 -0
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/README.md +0 -0
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/examples/README.md +0 -0
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/examples/sglang/multiturn_generate_sglang.py +0 -0
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/examples/sglang/online_multiturn_sglang.py +0 -0
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/examples/tinker/multiturn_generate_tinker.py +0 -0
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/examples/transformers/multiturn_generate_transformers.py +0 -0
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/examples/vllm/multiturn_generate_vllm.py +0 -0
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/pyproject.toml +0 -0
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/renderers/client.py +0 -0
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/renderers/deepseek_r1.py +0 -0
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/renderers/deepseek_v3.py +0 -0
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/renderers/default.py +0 -0
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/renderers/glm45.py +0 -0
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/renderers/glm5.py +0 -0
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/renderers/gpt_oss.py +0 -0
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/renderers/kimi_k2.py +0 -0
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/renderers/kimi_k25.py +0 -0
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/renderers/llama_3.py +0 -0
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/renderers/minimax_m2.py +0 -0
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/renderers/nemotron3.py +0 -0
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/renderers/parsers.py +0 -0
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/renderers/qwen3.py +0 -0
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/renderers/qwen35.py +0 -0
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/renderers/qwen36.py +0 -0
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/renderers/qwen3_vl.py +0 -0
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/tests/test_bridge.py +0 -0
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/tests/test_build_helpers.py +0 -0
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/tests/test_client.py +0 -0
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/tests/test_deepseek_r1.py +0 -0
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/tests/test_gpt_oss_harmony_parity.py +0 -0
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/tests/test_incremental.py +0 -0
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/tests/test_is_content.py +0 -0
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/tests/test_kimi_k25_tool_schema.py +0 -0
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/tests/test_llama_3.py +0 -0
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/tests/test_load_tokenizer.py +0 -0
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/tests/test_message_indices.py +0 -0
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/tests/test_message_tool_names.py +0 -0
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/tests/test_multimodal.py +0 -0
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/tests/test_nemotron3_parity.py +0 -0
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/tests/test_nemotron3_ultra.py +0 -0
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/tests/test_parse_response.py +0 -0
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/tests/test_parse_response_robustness.py +0 -0
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/tests/test_parsers.py +0 -0
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/tests/test_preserve_thinking.py +0 -0
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/tests/test_qwen35_size_coverage.py +0 -0
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/tests/test_render_ids.py +0 -0
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/tests/test_renderer_config.py +0 -0
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/tests/test_sampled_mask.py +0 -0
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/tests/test_tokens_per_message.py +0 -0
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/tests/test_tool_arg_type_preservation.py +0 -0
- {renderers-0.1.8.dev53 → renderers-0.1.8.dev55}/uv.lock +0 -0
|
@@ -35,6 +35,7 @@ chat-template kwargs. Those fields are covered by parity tests against
|
|
|
35
35
|
| Kimi K2 | `KimiK2RendererConfig` | - | `enable_thinking` |
|
|
36
36
|
| Kimi K2.5 / 2.6 | `KimiK25RendererConfig` | `thinking` | `image_cache_max` |
|
|
37
37
|
| Laguna XS.2 | `LagunaXS2RendererConfig` | `enable_thinking`, `render_assistant_messages_raw` | - |
|
|
38
|
+
| Laguna XS-2.1 | `LagunaXS21RendererConfig` | `enable_thinking` | - |
|
|
38
39
|
| Llama 3 | `Llama3RendererConfig` | `date_string`, `tools_in_user_message` | - |
|
|
39
40
|
| MiniMax M2 | `MiniMaxM2RendererConfig` | `model_identity` | - |
|
|
40
41
|
| Nemotron-3 Nano / Super | `Nemotron3RendererConfig` | `enable_thinking`, `truncate_history_thinking`, `low_effort` | - |
|
|
@@ -132,7 +133,7 @@ the knobs its template actually exposes:
|
|
|
132
133
|
| Nemotron-3 | `truncate_history_thinking=False -> all`; else `enable_thinking=False -> all`; else `tool_cycle` |
|
|
133
134
|
| DeepSeek R1 | `template` |
|
|
134
135
|
| MiniMax M2 | `tool_cycle` |
|
|
135
|
-
| DeepSeek V3, Qwen3-VL, Kimi K2, Laguna XS.2, Llama 3 | `all` |
|
|
136
|
+
| DeepSeek V3, Qwen3-VL, Kimi K2, Laguna XS.2 / XS-2.1, Llama 3 | `all` |
|
|
136
137
|
|
|
137
138
|
Config construction raises when an explicit template knob directly contradicts
|
|
138
139
|
an explicit generic bridge policy. For example:
|
|
@@ -54,6 +54,7 @@ from renderers.configs import (
|
|
|
54
54
|
KimiK25RendererConfig,
|
|
55
55
|
KimiK2RendererConfig,
|
|
56
56
|
LagunaXS2RendererConfig,
|
|
57
|
+
LagunaXS21RendererConfig,
|
|
57
58
|
Llama3RendererConfig,
|
|
58
59
|
MiniMaxM2RendererConfig,
|
|
59
60
|
Nemotron3RendererConfig,
|
|
@@ -86,6 +87,7 @@ _LAZY_RENDERERS: dict[str, str] = {
|
|
|
86
87
|
"GptOssRenderer": "renderers.gpt_oss",
|
|
87
88
|
"KimiK25Renderer": "renderers.kimi_k25",
|
|
88
89
|
"KimiK2Renderer": "renderers.kimi_k2",
|
|
90
|
+
"LagunaXS21Renderer": "renderers.laguna_xs2",
|
|
89
91
|
"LagunaXS2Renderer": "renderers.laguna_xs2",
|
|
90
92
|
"Llama3Renderer": "renderers.llama_3",
|
|
91
93
|
"MiniMaxM2Renderer": "renderers.minimax_m2",
|
|
@@ -139,6 +141,8 @@ __all__ = [
|
|
|
139
141
|
"KimiK2RendererConfig",
|
|
140
142
|
"LagunaXS2Renderer",
|
|
141
143
|
"LagunaXS2RendererConfig",
|
|
144
|
+
"LagunaXS21Renderer",
|
|
145
|
+
"LagunaXS21RendererConfig",
|
|
142
146
|
"Llama3Renderer",
|
|
143
147
|
"Llama3RendererConfig",
|
|
144
148
|
"MULTIMODAL_MODELS",
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.1.8.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 1, 8, '
|
|
21
|
+
__version__ = version = '0.1.8.dev55'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 8, 'dev55')
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -1060,8 +1060,10 @@ MODEL_RENDERER_MAP: dict[str, str] = {
|
|
|
1060
1060
|
# construction to pin a different date.
|
|
1061
1061
|
"meta-llama/Llama-3.2-1B-Instruct": "llama-3",
|
|
1062
1062
|
"meta-llama/Llama-3.2-3B-Instruct": "llama-3",
|
|
1063
|
-
# Poolside Laguna.
|
|
1063
|
+
# Poolside Laguna. The two checkpoints ship different chat templates,
|
|
1064
|
+
# each mirrored by its own renderer class.
|
|
1064
1065
|
"poolside/Laguna-XS.2": "laguna-xs.2",
|
|
1066
|
+
"poolside/Laguna-XS-2.1": "laguna-xs-2.1",
|
|
1065
1067
|
# GPT-OSS.
|
|
1066
1068
|
"openai/gpt-oss-20b": "gpt-oss",
|
|
1067
1069
|
"openai/gpt-oss-120b": "gpt-oss",
|
|
@@ -1302,7 +1304,7 @@ def _populate_registry():
|
|
|
1302
1304
|
from renderers.gpt_oss import GptOssRenderer
|
|
1303
1305
|
from renderers.kimi_k2 import KimiK2Renderer
|
|
1304
1306
|
from renderers.kimi_k25 import KimiK25Renderer
|
|
1305
|
-
from renderers.laguna_xs2 import LagunaXS2Renderer
|
|
1307
|
+
from renderers.laguna_xs2 import LagunaXS2Renderer, LagunaXS21Renderer
|
|
1306
1308
|
from renderers.llama_3 import Llama3Renderer
|
|
1307
1309
|
from renderers.minimax_m2 import MiniMaxM2Renderer
|
|
1308
1310
|
from renderers.nemotron3 import Nemotron3Renderer, Nemotron3UltraRenderer
|
|
@@ -1327,6 +1329,7 @@ def _populate_registry():
|
|
|
1327
1329
|
"kimi-k2": KimiK2Renderer,
|
|
1328
1330
|
"kimi-k2.5": KimiK25Renderer,
|
|
1329
1331
|
"laguna-xs.2": LagunaXS2Renderer,
|
|
1332
|
+
"laguna-xs-2.1": LagunaXS21Renderer,
|
|
1330
1333
|
"llama-3": Llama3Renderer,
|
|
1331
1334
|
"nemotron-3": Nemotron3Renderer,
|
|
1332
1335
|
"nemotron-3-ultra": Nemotron3UltraRenderer,
|
|
@@ -1761,6 +1764,8 @@ def _get_offset_tokenizer(tokenizer):
|
|
|
1761
1764
|
def attribute_text_segments(
|
|
1762
1765
|
tokenizer,
|
|
1763
1766
|
segments: "list[tuple[str, bool]]",
|
|
1767
|
+
*,
|
|
1768
|
+
overlap_is_content: bool = False,
|
|
1764
1769
|
) -> "list[tuple[int, bool]]":
|
|
1765
1770
|
"""Tokenize concatenated segments as a single BPE pass and return
|
|
1766
1771
|
``(token_id, is_content)`` pairs.
|
|
@@ -1779,6 +1784,16 @@ def attribute_text_segments(
|
|
|
1779
1784
|
tokens (rare; usually pre-tokenizer artefacts) are attributed to
|
|
1780
1785
|
the most recently entered segment.
|
|
1781
1786
|
|
|
1787
|
+
``overlap_is_content=True`` widens the content bit: a token counts
|
|
1788
|
+
as content when *any* of its source characters fall in a content
|
|
1789
|
+
segment, not just its first. Templates whose wrap glues directly
|
|
1790
|
+
onto the body with no whitespace (e.g. ``<user>{content}</user>``)
|
|
1791
|
+
can merge wrap and body bytes into one token; under the first-char
|
|
1792
|
+
policy such a token would land on the wrap side and the body would
|
|
1793
|
+
no longer be recoverable from the content run. Over-inclusion keeps
|
|
1794
|
+
every body byte inside the ``is_content=True`` run at the cost of a
|
|
1795
|
+
few adjacent wrap bytes.
|
|
1796
|
+
|
|
1782
1797
|
Requires a HuggingFace fast tokenizer with offset tracking. Every
|
|
1783
1798
|
model in ``MODEL_RENDERER_MAP`` ships one, so the offset lookup
|
|
1784
1799
|
always succeeds for tokenizers obtained via :func:`load_tokenizer`.
|
|
@@ -1815,13 +1830,25 @@ def attribute_text_segments(
|
|
|
1815
1830
|
|
|
1816
1831
|
out: list[tuple[int, bool]] = []
|
|
1817
1832
|
last_is_content = spans[-1][2] if spans else False
|
|
1818
|
-
for tok_id, (start,
|
|
1833
|
+
for tok_id, (start, end) in zip(token_ids, offsets):
|
|
1819
1834
|
if start >= total_len:
|
|
1820
1835
|
# Token's character offset is past every segment (shouldn't
|
|
1821
1836
|
# normally happen for add_special_tokens=False, but defensive
|
|
1822
1837
|
# against tokenizer-specific edge cases).
|
|
1823
1838
|
out.append((tok_id, last_is_content))
|
|
1824
1839
|
continue
|
|
1840
|
+
if overlap_is_content and end > start:
|
|
1841
|
+
out.append(
|
|
1842
|
+
(
|
|
1843
|
+
tok_id,
|
|
1844
|
+
any(
|
|
1845
|
+
seg_is_content
|
|
1846
|
+
for seg_start, seg_end, seg_is_content in spans
|
|
1847
|
+
if seg_start < end and start < seg_end
|
|
1848
|
+
),
|
|
1849
|
+
)
|
|
1850
|
+
)
|
|
1851
|
+
continue
|
|
1825
1852
|
# Find the segment that contains `start`. Segments are
|
|
1826
1853
|
# contiguous and ordered, so a linear scan is fine — the inner
|
|
1827
1854
|
# loop runs at most len(segments) times per token and segments
|
|
@@ -422,6 +422,25 @@ class LagunaXS2RendererConfig(BaseRendererConfig):
|
|
|
422
422
|
chat template's ``render_assistant_messages_raw`` gate."""
|
|
423
423
|
|
|
424
424
|
|
|
425
|
+
class LagunaXS21RendererConfig(BaseRendererConfig):
|
|
426
|
+
"""Laguna XS-2.1 renderer config.
|
|
427
|
+
|
|
428
|
+
XS-2.1's chat template reads a single kwarg, ``enable_thinking``,
|
|
429
|
+
which gates both the generation prompt (``<think>`` vs ``</think>``)
|
|
430
|
+
and whether assistant reasoning is rendered into the history at all.
|
|
431
|
+
Served by :class:`renderers.laguna_xs2.LagunaXS21Renderer`.
|
|
432
|
+
"""
|
|
433
|
+
|
|
434
|
+
name: Literal["laguna-xs-2.1"] = "laguna-xs-2.1"
|
|
435
|
+
|
|
436
|
+
enable_thinking: bool = False
|
|
437
|
+
"""When ``True``, the generation prompt ends with ``<think>`` and
|
|
438
|
+
every assistant turn renders ``<think>{reasoning}</think>``; when
|
|
439
|
+
``False``, turns open with a bare ``</think>`` and reasoning is not
|
|
440
|
+
rendered. Mirrors the template's ``enable_thinking`` kwarg and its
|
|
441
|
+
upstream default."""
|
|
442
|
+
|
|
443
|
+
|
|
425
444
|
class Llama3RendererConfig(BaseRendererConfig):
|
|
426
445
|
"""Llama-3.x Instruct renderer config.
|
|
427
446
|
|
|
@@ -576,6 +595,7 @@ RendererConfig = Annotated[
|
|
|
576
595
|
KimiK2RendererConfig,
|
|
577
596
|
KimiK25RendererConfig,
|
|
578
597
|
LagunaXS2RendererConfig,
|
|
598
|
+
LagunaXS21RendererConfig,
|
|
579
599
|
Llama3RendererConfig,
|
|
580
600
|
MiniMaxM2RendererConfig,
|
|
581
601
|
Nemotron3RendererConfig,
|
|
@@ -613,6 +633,7 @@ _CONFIG_BY_NAME: dict[str, type[BaseRendererConfig]] = {
|
|
|
613
633
|
"kimi-k2": KimiK2RendererConfig,
|
|
614
634
|
"kimi-k2.5": KimiK25RendererConfig,
|
|
615
635
|
"laguna-xs.2": LagunaXS2RendererConfig,
|
|
636
|
+
"laguna-xs-2.1": LagunaXS21RendererConfig,
|
|
616
637
|
"llama-3": Llama3RendererConfig,
|
|
617
638
|
"minimax-m2": MiniMaxM2RendererConfig,
|
|
618
639
|
"nemotron-3": Nemotron3RendererConfig,
|
|
@@ -659,6 +680,7 @@ __all__ = [
|
|
|
659
680
|
"KimiK25RendererConfig",
|
|
660
681
|
"KimiK2RendererConfig",
|
|
661
682
|
"LagunaXS2RendererConfig",
|
|
683
|
+
"LagunaXS21RendererConfig",
|
|
662
684
|
"Llama3RendererConfig",
|
|
663
685
|
"MiniMaxM2RendererConfig",
|
|
664
686
|
"Nemotron3RendererConfig",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Laguna-XS.2 Renderer.
|
|
1
|
+
"""Laguna-XS.2 / XS-2.1 Renderer.
|
|
2
2
|
|
|
3
3
|
Main properties:
|
|
4
4
|
- Prefix is the single token ``〈|EOS|〉`` (also the EOS / stop token).
|
|
@@ -12,13 +12,30 @@ Main properties:
|
|
|
12
12
|
- Tool calls: ``<tool_call>`` / ``</tool_call>`` ARE single tokens, but the
|
|
13
13
|
inner ``<arg_key>`` / ``</arg_key>`` / ``<arg_value>`` / ``</arg_value>``
|
|
14
14
|
markers are plain text — parsed via regex on the decoded inner block.
|
|
15
|
-
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
- Both templates bake in the same default Poolside system prompt when
|
|
16
|
+
``messages[0]`` is not a system message; a caller-supplied system
|
|
17
|
+
message overrides it, and an *empty* one opts out of the ``<system>``
|
|
18
|
+
block entirely (absent tools). The system block also contains the
|
|
19
|
+
tools section (under a ``### Tools`` header with an
|
|
20
|
+
``<available_tools>`` listing).
|
|
19
21
|
- Reasoning is rendered for every assistant message — no last-user-index
|
|
20
22
|
gating. ``thinking_retention`` is accepted for protocol uniformity but
|
|
21
23
|
is effectively a no-op since past reasoning is preserved by default.
|
|
24
|
+
|
|
25
|
+
XS-2.1's template (upstream rev ``575f0f28``) is served by the
|
|
26
|
+
:class:`LagunaXS21Renderer` subclass below:
|
|
27
|
+
|
|
28
|
+
- Role tags hug their content: ``<user>{content}</user>``, no inner
|
|
29
|
+
newlines.
|
|
30
|
+
- Assistant reasoning is gated on ``enable_thinking``: on, the turn
|
|
31
|
+
opens ``<think>{reasoning}</think>`` verbatim (empty reasoning
|
|
32
|
+
included); off, it opens with a bare ``</think>`` and message
|
|
33
|
+
reasoning is not rendered. Content and tool-call args render verbatim.
|
|
34
|
+
- Tool-call args pack tightly
|
|
35
|
+
(``<arg_key>k</arg_key><arg_value>v</arg_value>``) and the tools
|
|
36
|
+
section ends at ``</available_tools>``.
|
|
37
|
+
- The ``<system>`` block is emitted whenever there is system content,
|
|
38
|
+
tools, or ``enable_thinking`` — even if that leaves it empty.
|
|
22
39
|
"""
|
|
23
40
|
|
|
24
41
|
from __future__ import annotations
|
|
@@ -39,7 +56,7 @@ from renderers.base import (
|
|
|
39
56
|
resolve_thinking_retention,
|
|
40
57
|
should_rerender_for_thinking_retention,
|
|
41
58
|
)
|
|
42
|
-
from renderers.configs import LagunaXS2RendererConfig
|
|
59
|
+
from renderers.configs import LagunaXS2RendererConfig, LagunaXS21RendererConfig
|
|
43
60
|
from renderers.parsing import parse_laguna_xs2
|
|
44
61
|
|
|
45
62
|
_DEFAULT_SYSTEM_MESSAGE = (
|
|
@@ -76,12 +93,21 @@ _TOOLS_FOOTER_NO_THINKING = (
|
|
|
76
93
|
"</tool_call>"
|
|
77
94
|
)
|
|
78
95
|
|
|
96
|
+
# XS-2.1 tools section: this header, one tojson line per tool, then a
|
|
97
|
+
# bare "</available_tools>" close.
|
|
98
|
+
_TOOLS_HEADER_XS21 = (
|
|
99
|
+
"### Tools\n\n"
|
|
100
|
+
"You may call functions to assist with the user query.\n"
|
|
101
|
+
"All available function signatures are listed below:\n"
|
|
102
|
+
"<available_tools>\n"
|
|
103
|
+
)
|
|
104
|
+
|
|
79
105
|
|
|
80
106
|
class LagunaXS2Renderer:
|
|
81
107
|
def __init__(
|
|
82
108
|
self,
|
|
83
109
|
tokenizer: PreTrainedTokenizer,
|
|
84
|
-
config: LagunaXS2RendererConfig | None = None,
|
|
110
|
+
config: LagunaXS2RendererConfig | LagunaXS21RendererConfig | None = None,
|
|
85
111
|
):
|
|
86
112
|
self._tokenizer = tokenizer
|
|
87
113
|
self.config = config or LagunaXS2RendererConfig()
|
|
@@ -89,6 +115,10 @@ class LagunaXS2Renderer:
|
|
|
89
115
|
self.config,
|
|
90
116
|
"all",
|
|
91
117
|
)
|
|
118
|
+
# Both templates bake in the same default Poolside system prompt;
|
|
119
|
+
# an empty caller-supplied system message opts out of the
|
|
120
|
+
# <system> block (each variant's render mirrors its own gate).
|
|
121
|
+
self._default_system_message = _DEFAULT_SYSTEM_MESSAGE
|
|
92
122
|
|
|
93
123
|
self._eos = self._token_id("〈|EOS|〉")
|
|
94
124
|
self._think = self._token_id("<think>")
|
|
@@ -184,7 +214,7 @@ class LagunaXS2Renderer:
|
|
|
184
214
|
emit_special(self._eos, -1, is_sampled=False, is_content=False)
|
|
185
215
|
|
|
186
216
|
# ── System header (absorbs messages[0] if it's a system message) ──
|
|
187
|
-
system_content =
|
|
217
|
+
system_content = self._default_system_message
|
|
188
218
|
system_msg_idx = -1
|
|
189
219
|
caller_has_system = bool(messages and messages[0].get("role") == "system")
|
|
190
220
|
if caller_has_system:
|
|
@@ -196,16 +226,12 @@ class LagunaXS2Renderer:
|
|
|
196
226
|
# gate: when the caller passes an empty system message and no tools,
|
|
197
227
|
# the whole ``<system>...</system>`` block is omitted.
|
|
198
228
|
if has_sys_content or tools:
|
|
199
|
-
# The template emits ``<system>\n`` then conditionally a second
|
|
200
|
-
# ``\n``. Bundle those into one emit so BPE merges ``\n\n`` into
|
|
201
|
-
# its single-token form (rather than two ``\n`` atoms).
|
|
202
|
-
emit_text(
|
|
203
|
-
"<system>\n\n" if has_sys_content else "<system>\n",
|
|
204
|
-
-1,
|
|
205
|
-
is_sampled=False,
|
|
206
|
-
is_content=False,
|
|
207
|
-
)
|
|
208
229
|
if has_sys_content:
|
|
230
|
+
# The template emits ``<system>\n`` then a second ``\n``
|
|
231
|
+
# before the system body. Bundle those into one emit so BPE
|
|
232
|
+
# merges ``\n\n`` into its single-token form (rather than
|
|
233
|
+
# two ``\n`` atoms).
|
|
234
|
+
emit_text("<system>\n\n", -1, is_sampled=False, is_content=False)
|
|
209
235
|
# If the caller provided system content, it's body bytes;
|
|
210
236
|
# otherwise this is the default system prompt (scaffold).
|
|
211
237
|
sys_is_content = caller_has_system
|
|
@@ -224,6 +250,11 @@ class LagunaXS2Renderer:
|
|
|
224
250
|
if self.config.enable_thinking
|
|
225
251
|
else _TOOLS_FOOTER_NO_THINKING
|
|
226
252
|
)
|
|
253
|
+
if not has_sys_content:
|
|
254
|
+
# No system body: ``<system>\n`` runs straight into the
|
|
255
|
+
# tools header's ``\n\n`` — encode them together so BPE
|
|
256
|
+
# merges the ``\n\n\n`` seam as the template does.
|
|
257
|
+
tool_text = "<system>\n" + tool_text
|
|
227
258
|
emit_text(tool_text, -1, is_sampled=False, is_content=False)
|
|
228
259
|
emit_text("\n</system>\n", -1, is_sampled=False, is_content=False)
|
|
229
260
|
|
|
@@ -450,7 +481,12 @@ class LagunaXS2Renderer:
|
|
|
450
481
|
emit_text,
|
|
451
482
|
emit_text_segments,
|
|
452
483
|
) -> None:
|
|
453
|
-
|
|
484
|
+
# Raw passthrough is an XS.2-only template gate; the XS-2.1
|
|
485
|
+
# config doesn't define it.
|
|
486
|
+
if (
|
|
487
|
+
isinstance(self.config, LagunaXS2RendererConfig)
|
|
488
|
+
and self.config.render_assistant_messages_raw
|
|
489
|
+
):
|
|
454
490
|
self._render_assistant_raw(
|
|
455
491
|
msg_idx,
|
|
456
492
|
content,
|
|
@@ -582,3 +618,347 @@ class LagunaXS2Renderer:
|
|
|
582
618
|
emit_text("\n", msg_idx, is_sampled=False, is_content=False)
|
|
583
619
|
emit_special(self._assistant_end, msg_idx, is_sampled=True, is_content=True)
|
|
584
620
|
emit_text("\n", msg_idx, is_sampled=False, is_content=False)
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
class LagunaXS21Renderer(LagunaXS2Renderer):
|
|
624
|
+
"""Laguna-XS-2.1 — mirrors the ``poolside/Laguna-XS-2.1`` chat
|
|
625
|
+
template (upstream rev ``575f0f28``); see the module docstring for
|
|
626
|
+
its format.
|
|
627
|
+
|
|
628
|
+
Token wiring, stop tokens, and the parsing skeleton are shared with
|
|
629
|
+
:class:`LagunaXS2Renderer`; ``render``, ``bridge_to_next_turn``, and
|
|
630
|
+
the assistant emit implement this template's format.
|
|
631
|
+
"""
|
|
632
|
+
|
|
633
|
+
def __init__(
|
|
634
|
+
self,
|
|
635
|
+
tokenizer: PreTrainedTokenizer,
|
|
636
|
+
config: LagunaXS21RendererConfig | None = None,
|
|
637
|
+
):
|
|
638
|
+
super().__init__(tokenizer, config or LagunaXS21RendererConfig())
|
|
639
|
+
|
|
640
|
+
def render(
|
|
641
|
+
self,
|
|
642
|
+
messages: list[Message],
|
|
643
|
+
*,
|
|
644
|
+
tools: list[ToolSpec] | None = None,
|
|
645
|
+
add_generation_prompt: bool = False,
|
|
646
|
+
) -> RenderedTokens:
|
|
647
|
+
if not messages:
|
|
648
|
+
raise ValueError("No messages provided.")
|
|
649
|
+
|
|
650
|
+
tokens: list[int] = []
|
|
651
|
+
indices: list[int] = []
|
|
652
|
+
sampled: list[bool] = []
|
|
653
|
+
content_mask: list[bool] = []
|
|
654
|
+
|
|
655
|
+
def emit_special(
|
|
656
|
+
token_id: int, msg_idx: int, *, is_sampled: bool, is_content: bool
|
|
657
|
+
) -> None:
|
|
658
|
+
tokens.append(token_id)
|
|
659
|
+
indices.append(msg_idx)
|
|
660
|
+
sampled.append(is_sampled)
|
|
661
|
+
content_mask.append(is_content)
|
|
662
|
+
|
|
663
|
+
def emit_text(
|
|
664
|
+
text: str, msg_idx: int, *, is_sampled: bool, is_content: bool
|
|
665
|
+
) -> None:
|
|
666
|
+
ids = self._encode(text)
|
|
667
|
+
tokens.extend(ids)
|
|
668
|
+
indices.extend([msg_idx] * len(ids))
|
|
669
|
+
sampled.extend([is_sampled] * len(ids))
|
|
670
|
+
content_mask.extend([is_content] * len(ids))
|
|
671
|
+
|
|
672
|
+
def emit_text_segments(
|
|
673
|
+
segments: list[tuple[str, bool]], msg_idx: int, *, is_sampled: bool
|
|
674
|
+
) -> None:
|
|
675
|
+
# Role tags hug the body with no whitespace, so a BPE merge
|
|
676
|
+
# can pull wrap bytes and body bytes into one token —
|
|
677
|
+
# overlap attribution keeps every body byte in the content
|
|
678
|
+
# run.
|
|
679
|
+
for tok_id, is_content in attribute_text_segments(
|
|
680
|
+
self._tokenizer, segments, overlap_is_content=True
|
|
681
|
+
):
|
|
682
|
+
tokens.append(tok_id)
|
|
683
|
+
indices.append(msg_idx)
|
|
684
|
+
sampled.append(is_sampled)
|
|
685
|
+
content_mask.append(is_content)
|
|
686
|
+
|
|
687
|
+
emit_special(self._eos, -1, is_sampled=False, is_content=False)
|
|
688
|
+
|
|
689
|
+
# ── System header (absorbs messages[0] if it's a system message) ──
|
|
690
|
+
system_content = self._default_system_message
|
|
691
|
+
caller_has_system = bool(messages and messages[0].get("role") == "system")
|
|
692
|
+
if caller_has_system:
|
|
693
|
+
system_content = self._visible_text(messages[0].get("content"))
|
|
694
|
+
|
|
695
|
+
has_sys_content = bool(system_content and system_content.strip())
|
|
696
|
+
# The template's gate is ``has_sys or tools or enable_thinking`` —
|
|
697
|
+
# an empty caller system message opts out of the default, and with
|
|
698
|
+
# neither tools nor thinking the block vanishes entirely.
|
|
699
|
+
if has_sys_content or tools or self.config.enable_thinking:
|
|
700
|
+
# The whole header is one plain-text run — ``<system>`` glues
|
|
701
|
+
# straight onto the body with no newline — so it must be
|
|
702
|
+
# tokenized in a single BPE pass. In the header, content bytes
|
|
703
|
+
# exist exactly when the caller supplied the system message
|
|
704
|
+
# (the default prompt is scaffold), so the is_content bit also
|
|
705
|
+
# selects the message index: body → 0, everything else → -1.
|
|
706
|
+
header_segs: list[tuple[str, bool]] = [("<system>", False)]
|
|
707
|
+
if has_sys_content:
|
|
708
|
+
header_segs.append((system_content.rstrip(), caller_has_system))
|
|
709
|
+
if tools:
|
|
710
|
+
header_segs.append(("\n\n", False))
|
|
711
|
+
if tools:
|
|
712
|
+
tool_text = _TOOLS_HEADER_XS21
|
|
713
|
+
for tool in tools:
|
|
714
|
+
tool_text += json.dumps(tool, ensure_ascii=False) + "\n"
|
|
715
|
+
tool_text += "</available_tools>"
|
|
716
|
+
header_segs.append((tool_text, False))
|
|
717
|
+
header_segs.append(("</system>\n", False))
|
|
718
|
+
for tok_id, is_content in attribute_text_segments(
|
|
719
|
+
self._tokenizer, header_segs, overlap_is_content=True
|
|
720
|
+
):
|
|
721
|
+
emit_special(
|
|
722
|
+
tok_id,
|
|
723
|
+
0 if is_content else -1,
|
|
724
|
+
is_sampled=False,
|
|
725
|
+
is_content=is_content,
|
|
726
|
+
)
|
|
727
|
+
|
|
728
|
+
# ── Per-message loop ──────────────────────────────────────────
|
|
729
|
+
for i, msg in enumerate(messages):
|
|
730
|
+
content = self._visible_text(msg.get("content"))
|
|
731
|
+
|
|
732
|
+
match msg["role"]:
|
|
733
|
+
case "system":
|
|
734
|
+
# The template slices a leading system message off the
|
|
735
|
+
# loop (it lives in the header); later ones render.
|
|
736
|
+
if i == 0:
|
|
737
|
+
continue
|
|
738
|
+
sys_segs: list[tuple[str, bool]] = [("<system>", False)]
|
|
739
|
+
if content:
|
|
740
|
+
sys_segs.append((content, True))
|
|
741
|
+
sys_segs.append(("</system>\n", False))
|
|
742
|
+
emit_text_segments(sys_segs, i, is_sampled=False)
|
|
743
|
+
case "user":
|
|
744
|
+
user_segs: list[tuple[str, bool]] = [("<user>", False)]
|
|
745
|
+
if content:
|
|
746
|
+
user_segs.append((content, True))
|
|
747
|
+
user_segs.append(("</user>\n", False))
|
|
748
|
+
emit_text_segments(user_segs, i, is_sampled=False)
|
|
749
|
+
case "assistant":
|
|
750
|
+
self._render_assistant(
|
|
751
|
+
msg,
|
|
752
|
+
i,
|
|
753
|
+
content,
|
|
754
|
+
emit_special=emit_special,
|
|
755
|
+
emit_text=emit_text,
|
|
756
|
+
emit_text_segments=emit_text_segments,
|
|
757
|
+
)
|
|
758
|
+
case "tool":
|
|
759
|
+
tool_segs: list[tuple[str, bool]] = [("<tool_response>", False)]
|
|
760
|
+
if content:
|
|
761
|
+
tool_segs.append((content, True))
|
|
762
|
+
tool_segs.append(("</tool_response>\n", False))
|
|
763
|
+
emit_text_segments(tool_segs, i, is_sampled=False)
|
|
764
|
+
|
|
765
|
+
# ── Generation prompt (no newline after <assistant>) ──────────
|
|
766
|
+
if add_generation_prompt:
|
|
767
|
+
emit_special(self._assistant, -1, is_sampled=False, is_content=False)
|
|
768
|
+
if self.config.enable_thinking:
|
|
769
|
+
emit_special(self._think, -1, is_sampled=False, is_content=False)
|
|
770
|
+
else:
|
|
771
|
+
emit_special(self._think_end, -1, is_sampled=False, is_content=False)
|
|
772
|
+
|
|
773
|
+
return RenderedTokens(
|
|
774
|
+
token_ids=tokens,
|
|
775
|
+
message_indices=indices,
|
|
776
|
+
sampled_mask=sampled,
|
|
777
|
+
is_content=content_mask,
|
|
778
|
+
message_roles=[m.get("role") or "" for m in messages],
|
|
779
|
+
message_tool_names=extract_message_tool_names(messages),
|
|
780
|
+
)
|
|
781
|
+
|
|
782
|
+
def parse_response(
|
|
783
|
+
self,
|
|
784
|
+
token_ids: list[int],
|
|
785
|
+
*,
|
|
786
|
+
tools: list[ToolSpec] | None = None,
|
|
787
|
+
) -> ParsedResponse:
|
|
788
|
+
# The XS-2.1 template renders reasoning and content verbatim (no
|
|
789
|
+
# newline wrapping), so the parse is verbatim too.
|
|
790
|
+
return parse_laguna_xs2(
|
|
791
|
+
self._tokenizer,
|
|
792
|
+
token_ids,
|
|
793
|
+
stop_ids={self._assistant_end, self._eos},
|
|
794
|
+
think_id=self._think,
|
|
795
|
+
think_end_id=self._think_end,
|
|
796
|
+
tool_call_id=self._tool_call,
|
|
797
|
+
tool_call_end_id=self._tool_call_end,
|
|
798
|
+
tools=tools,
|
|
799
|
+
strip_newlines=False,
|
|
800
|
+
)
|
|
801
|
+
|
|
802
|
+
def bridge_to_next_turn(
|
|
803
|
+
self,
|
|
804
|
+
previous_prompt_ids: list[int],
|
|
805
|
+
previous_completion_ids: list[int],
|
|
806
|
+
new_messages: list[Message],
|
|
807
|
+
*,
|
|
808
|
+
tools: list[ToolSpec] | None = None,
|
|
809
|
+
) -> RenderedTokens | None:
|
|
810
|
+
if (
|
|
811
|
+
not previous_prompt_ids
|
|
812
|
+
or not new_messages
|
|
813
|
+
or reject_assistant_in_extension(new_messages)
|
|
814
|
+
):
|
|
815
|
+
return None
|
|
816
|
+
if should_rerender_for_thinking_retention(
|
|
817
|
+
self.effective_thinking_retention,
|
|
818
|
+
new_messages,
|
|
819
|
+
):
|
|
820
|
+
return None
|
|
821
|
+
|
|
822
|
+
# ``</assistant>`` is the canonical turn close; ``〈|EOS|〉`` also
|
|
823
|
+
# stops generation. Truncation (no stop token at the tail)
|
|
824
|
+
# synthesises the close. The inter-turn ``\n`` the template puts
|
|
825
|
+
# after ``</assistant>`` is prepended to the first extension
|
|
826
|
+
# message below so the seam encodes with the tag run.
|
|
827
|
+
previous_ids = list(previous_prompt_ids) + list(previous_completion_ids)
|
|
828
|
+
stop_ids = {self._assistant_end, self._eos}
|
|
829
|
+
if (
|
|
830
|
+
not previous_ids[len(previous_prompt_ids) :]
|
|
831
|
+
or previous_ids[-1] not in stop_ids
|
|
832
|
+
):
|
|
833
|
+
previous_ids.append(self._assistant_end)
|
|
834
|
+
|
|
835
|
+
ext: list[int] = []
|
|
836
|
+
ext_indices: list[int] = []
|
|
837
|
+
ext_sampled: list[bool] = []
|
|
838
|
+
ext_content: list[bool] = []
|
|
839
|
+
|
|
840
|
+
def emit_special(
|
|
841
|
+
token_id: int,
|
|
842
|
+
msg_idx: int = -1,
|
|
843
|
+
*,
|
|
844
|
+
is_sampled: bool = False,
|
|
845
|
+
is_content: bool = False,
|
|
846
|
+
) -> None:
|
|
847
|
+
ext.append(token_id)
|
|
848
|
+
ext_indices.append(msg_idx)
|
|
849
|
+
ext_sampled.append(is_sampled)
|
|
850
|
+
ext_content.append(is_content)
|
|
851
|
+
|
|
852
|
+
def emit_text_segments(
|
|
853
|
+
segments: list[tuple[str, bool]],
|
|
854
|
+
msg_idx: int = -1,
|
|
855
|
+
*,
|
|
856
|
+
is_sampled: bool = False,
|
|
857
|
+
) -> None:
|
|
858
|
+
for tok_id, is_content in attribute_text_segments(
|
|
859
|
+
self._tokenizer, segments, overlap_is_content=True
|
|
860
|
+
):
|
|
861
|
+
ext.append(tok_id)
|
|
862
|
+
ext_indices.append(msg_idx)
|
|
863
|
+
ext_sampled.append(is_sampled)
|
|
864
|
+
ext_content.append(is_content)
|
|
865
|
+
|
|
866
|
+
_OPEN = {"user": "<user>", "system": "<system>", "tool": "<tool_response>"}
|
|
867
|
+
_CLOSE = {
|
|
868
|
+
"user": "</user>\n",
|
|
869
|
+
"system": "</system>\n",
|
|
870
|
+
"tool": "</tool_response>\n",
|
|
871
|
+
}
|
|
872
|
+
for i, msg in enumerate(new_messages):
|
|
873
|
+
role = msg.get("role")
|
|
874
|
+
if role not in _OPEN:
|
|
875
|
+
return None
|
|
876
|
+
content = self._visible_text(msg.get("content"))
|
|
877
|
+
lead = "\n" if i == 0 else ""
|
|
878
|
+
segs: list[tuple[str, bool]] = [(lead + _OPEN[role], False)]
|
|
879
|
+
if content:
|
|
880
|
+
segs.append((content, True))
|
|
881
|
+
segs.append((_CLOSE[role], False))
|
|
882
|
+
emit_text_segments(segs, i)
|
|
883
|
+
|
|
884
|
+
emit_special(self._assistant, -1)
|
|
885
|
+
if self.config.enable_thinking:
|
|
886
|
+
emit_special(self._think, -1)
|
|
887
|
+
else:
|
|
888
|
+
emit_special(self._think_end, -1)
|
|
889
|
+
|
|
890
|
+
total_len = len(previous_ids) + len(ext)
|
|
891
|
+
return RenderedTokens(
|
|
892
|
+
token_ids=previous_ids + ext,
|
|
893
|
+
message_indices=[-1] * len(previous_ids) + ext_indices,
|
|
894
|
+
sampled_mask=[False] * total_len,
|
|
895
|
+
is_content=[False] * len(previous_ids) + ext_content,
|
|
896
|
+
message_roles=[m.get("role") or "" for m in new_messages],
|
|
897
|
+
message_tool_names=extract_message_tool_names(new_messages),
|
|
898
|
+
)
|
|
899
|
+
|
|
900
|
+
def _render_assistant(
|
|
901
|
+
self,
|
|
902
|
+
msg: Message,
|
|
903
|
+
msg_idx: int,
|
|
904
|
+
content: str,
|
|
905
|
+
*,
|
|
906
|
+
emit_special,
|
|
907
|
+
emit_text,
|
|
908
|
+
emit_text_segments,
|
|
909
|
+
) -> None:
|
|
910
|
+
reasoning_content = ""
|
|
911
|
+
if isinstance(msg.get("reasoning_content"), str):
|
|
912
|
+
reasoning_content = msg["reasoning_content"]
|
|
913
|
+
else:
|
|
914
|
+
part_thinking = self._thinking_text(msg.get("content"))
|
|
915
|
+
if part_thinking:
|
|
916
|
+
reasoning_content = part_thinking
|
|
917
|
+
|
|
918
|
+
# ``<assistant>`` plus the think tag that follows are exactly the
|
|
919
|
+
# generation prompt for the active mode — template-injected
|
|
920
|
+
# scaffolding the model never samples.
|
|
921
|
+
emit_special(self._assistant, msg_idx, is_sampled=False, is_content=False)
|
|
922
|
+
|
|
923
|
+
if self.config.enable_thinking:
|
|
924
|
+
# ``<think>{reasoning}</think>`` renders verbatim, even when
|
|
925
|
+
# the reasoning is empty; the opener is the gen-prompt
|
|
926
|
+
# prefill, the rest the model sampled.
|
|
927
|
+
emit_special(self._think, msg_idx, is_sampled=False, is_content=False)
|
|
928
|
+
if reasoning_content:
|
|
929
|
+
emit_text(reasoning_content, msg_idx, is_sampled=True, is_content=True)
|
|
930
|
+
emit_special(self._think_end, msg_idx, is_sampled=True, is_content=True)
|
|
931
|
+
else:
|
|
932
|
+
# Thinking off: any reasoning on the message is dropped and
|
|
933
|
+
# the turn opens with the prefilled ``</think>``.
|
|
934
|
+
emit_special(self._think_end, msg_idx, is_sampled=False, is_content=False)
|
|
935
|
+
|
|
936
|
+
if content:
|
|
937
|
+
emit_text(content, msg_idx, is_sampled=True, is_content=True)
|
|
938
|
+
|
|
939
|
+
tool_calls = msg.get("tool_calls") or []
|
|
940
|
+
for tc in tool_calls:
|
|
941
|
+
func = tc.get("function") or tc
|
|
942
|
+
name = func.get("name", "")
|
|
943
|
+
arguments = func.get("arguments", {})
|
|
944
|
+
if isinstance(arguments, str):
|
|
945
|
+
try:
|
|
946
|
+
arguments = json.loads(arguments)
|
|
947
|
+
except json.JSONDecodeError:
|
|
948
|
+
arguments = {}
|
|
949
|
+
|
|
950
|
+
emit_special(self._tool_call, msg_idx, is_sampled=True, is_content=True)
|
|
951
|
+
inner = name
|
|
952
|
+
if isinstance(arguments, dict):
|
|
953
|
+
for k, v in arguments.items():
|
|
954
|
+
inner += "<arg_key>" + k + "</arg_key>"
|
|
955
|
+
if isinstance(v, str):
|
|
956
|
+
val_text = v
|
|
957
|
+
else:
|
|
958
|
+
val_text = json.dumps(v, ensure_ascii=False)
|
|
959
|
+
inner += "<arg_value>" + val_text + "</arg_value>"
|
|
960
|
+
emit_text(inner, msg_idx, is_sampled=True, is_content=True)
|
|
961
|
+
emit_special(self._tool_call_end, msg_idx, is_sampled=True, is_content=True)
|
|
962
|
+
|
|
963
|
+
emit_special(self._assistant_end, msg_idx, is_sampled=True, is_content=True)
|
|
964
|
+
emit_text("\n", msg_idx, is_sampled=False, is_content=False)
|