mirror of
https://github.com/cactus-compute/needle.git
synced 2026-10-20 06:00:31 +08:00
123 lines
4.4 KiB
Python
123 lines
4.4 KiB
Python
import json
|
|
|
|
import numpy as np
|
|
import pytest
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def tok():
|
|
from needle.model.tokenizer import get_tokenizer
|
|
return get_tokenizer(8192)
|
|
|
|
|
|
def test_render_example_markers():
|
|
from needle.model.finetune import render_example
|
|
from needle.model.tokenizer import IM_START, TOOLS_START, TOOL_CALL_START, THINK_START
|
|
|
|
example = {"tools": [{"name": "f", "parameters": {"type": "object", "properties": {}}}],
|
|
"query": "do the thing", "reasoning": "because reasons",
|
|
"answers": [{"name": "f", "arguments": {}}]}
|
|
prompt, target = render_example(example)
|
|
assert IM_START in prompt
|
|
assert TOOLS_START in prompt
|
|
assert "do the thing" in prompt
|
|
assert THINK_START in target
|
|
assert "because reasons" in target
|
|
assert TOOL_CALL_START in target
|
|
assert '"name":"f"' in target
|
|
|
|
|
|
def test_render_example_matches_training_template():
|
|
from needle.model.finetune import render_example
|
|
from needle.model.tokenizer import (IM_END, THINK_START, THINK_END,
|
|
TOOL_CALL_START, TOOL_CALL_END)
|
|
|
|
example = {"tools": [], "query": "q", "reasoning": "'q' -> f",
|
|
"answers": [{"name": "f", "arguments": {}}]}
|
|
_, target = render_example(example)
|
|
assert target == (THINK_START + "\n'q' -> f\n" + THINK_END + "\n"
|
|
+ TOOL_CALL_START + '[{"name":"f","arguments":{}}]'
|
|
+ TOOL_CALL_END + IM_END)
|
|
|
|
|
|
def test_render_example_empty_reasoning_omits_think():
|
|
from needle.model.finetune import render_example
|
|
from needle.model.tokenizer import THINK_START, TOOL_CALL_START
|
|
|
|
for example in ({"tools": [], "query": "q", "answers": []},
|
|
{"tools": [], "query": "q", "reasoning": "", "answers": []},
|
|
{"tools": [], "query": "q", "reasoning": None, "answers": []},
|
|
{"tools": [], "query": "q", "reasoning": " ", "answers": []}):
|
|
_, target = render_example(example)
|
|
assert THINK_START not in target
|
|
assert target.startswith(TOOL_CALL_START)
|
|
|
|
|
|
def test_render_example_keeps_unicode():
|
|
from needle.model.finetune import render_example
|
|
|
|
example = {"tools": [{"name": "f", "parameters": {"type": "object", "properties": {
|
|
"name": {"type": "string", "description": "café"}}}}],
|
|
"query": "add Tomás",
|
|
"answers": [{"name": "f", "arguments": {"name": "Tomás"}}]}
|
|
prompt, target = render_example(example)
|
|
assert "café" in prompt
|
|
assert '"Tomás"' in target
|
|
|
|
|
|
def test_render_example_accepts_function_calls_alias():
|
|
from needle.model.finetune import render_example
|
|
example = {"tools": [], "query": "hi",
|
|
"function_calls": [{"name": "g", "arguments": {"x": 1}}]}
|
|
_, target = render_example(example)
|
|
assert '"name":"g"' in target
|
|
|
|
|
|
def test_encode_loss_mask_targets_only(tok):
|
|
from needle.model.finetune import _encode, render_example
|
|
from needle.model.tokenizer import BOS_ID
|
|
|
|
example = {"tools": [{"name": "f", "parameters": {"type": "object", "properties": {}}}],
|
|
"query": "hello", "answers": [{"name": "f", "arguments": {}}]}
|
|
ids, mask = _encode(tok, example, max_len=128)
|
|
|
|
assert len(ids) == 128 and len(mask) == 128
|
|
assert ids[0] == BOS_ID
|
|
assert mask[0] == 0.0
|
|
assert any(m == 1.0 for m in mask)
|
|
|
|
_, target = render_example(example)
|
|
assert sum(1 for m in mask if m == 1.0) == len(tok.encode(target)) + 1
|
|
|
|
|
|
def test_fit_max_len_buckets(tok, tmp_path):
|
|
from needle.model.finetune import fit_max_len
|
|
|
|
path = tmp_path / "data.jsonl"
|
|
with open(path, "w") as handle:
|
|
handle.write(json.dumps({"tools": [], "query": "short", "answers": []}) + "\n")
|
|
|
|
assert fit_max_len(str(path), tok, 1024) == 128
|
|
assert fit_max_len(str(path), tok, 64) == 64
|
|
|
|
|
|
def test_load_jsonl_shapes_and_skips_invalid(tok, tmp_path):
|
|
from needle.model.finetune import load_jsonl
|
|
|
|
path = tmp_path / "data.jsonl"
|
|
rows = [
|
|
{"tools": [], "query": "a", "answers": []},
|
|
{"tools": [], "query": "b", "answers": []},
|
|
{"tools": [], "reasoning": "no query here"},
|
|
]
|
|
with open(path, "w") as handle:
|
|
for row in rows:
|
|
handle.write(json.dumps(row) + "\n")
|
|
handle.write("\n")
|
|
|
|
seqs, masks = load_jsonl(str(path), tok, max_len=32)
|
|
assert seqs.shape == (2, 32)
|
|
assert masks.shape == (2, 32)
|
|
assert seqs.dtype == np.int32
|
|
assert masks.dtype == np.float32
|