Download code/actionman/features.py from shing-dev/ActionMan-0.1: direct link, hf CLI and curl.
- Browser
- Download file 5.3 kB
-
https://huggingface.co/shing-dev/ActionMan-0.1/resolve/main/code/actionman/features.py
- Command line
-
hf download hf://shing-dev/ActionMan-0.1/code/actionman/features.py
-
curl -L -o features.py https://huggingface.co/shing-dev/ActionMan-0.1/resolve/main/code/actionman/features.py
5.3 kB
| """Tokenizer setup and the two sequence builders. | |
| Pass A: [CLS] [T] sig_1 [T] sig_2 ... [USER] query [CALL] x4 [SEP] | |
| Pass B: <pass A without [SEP]> [ARGS] tool( p= [MASK]*n , q= [MASK]*m ) ... [SEP] | |
| """ | |
| from __future__ import annotations | |
| from .schema import Example, signature_pieces, value_to_text | |
| SPECIAL = ["[T]", "[CALL]", "[ARGS]", "[USER]", "[OPT]"] # [OPT]: v3 option marker (appended, ids of the rest unchanged) | |
| MAX_CALLS = 4 | |
| BUCKETS = [0, 4, 8, 16, 32] # slot counts for bucket ids 0..4; id 5 = overflow | |
| OVERFLOW = 5 | |
| N_BUCKETS = 6 | |
| MAX_LEN_A = 896 | |
| MAX_LEN_B = 1024 | |
| def load_tokenizer(name: str = "answerdotai/ModernBERT-large"): | |
| from transformers import AutoTokenizer | |
| tok = AutoTokenizer.from_pretrained(name) | |
| tok.add_special_tokens({"additional_special_tokens": SPECIAL}) | |
| tok.am = {s: tok.convert_tokens_to_ids(s) for s in SPECIAL} | |
| return tok | |
| def bucket_of(n_tokens: int) -> int: | |
| for b in range(1, 5): | |
| if n_tokens <= BUCKETS[b]: | |
| return b | |
| return OVERFLOW | |
| def _enc(tok, text: str) -> list[int]: | |
| return tok(text, add_special_tokens=False).input_ids | |
| def value_tokens(tok, value) -> list[int]: | |
| return _enc(tok, value_to_text(value)) | |
| def build_pass_a(tok, ex: Example, desc_words: int = 8, max_len: int = MAX_LEN_A) -> dict | None: | |
| """Context sequence + decision / length labels. Returns None if it can't fit.""" | |
| for dw in (desc_words, 0): | |
| ids = [tok.cls_token_id] | |
| t_pos, arg_pos = [], {} | |
| for j, tool in enumerate(ex.tools): | |
| t_pos.append(len(ids)) | |
| ids.append(tok.am["[T]"]) | |
| for text, kind, k in signature_pieces(tool, dw): | |
| if kind == "param": | |
| arg_pos[(j, k)] = len(ids) | |
| ids += _enc(tok, text) | |
| ids.append(tok.am["[USER]"]) | |
| ids += _enc(tok, ex.query) | |
| call_pos = list(range(len(ids), len(ids) + MAX_CALLS)) | |
| ids += [tok.am["[CALL]"]] * MAX_CALLS + [tok.sep_token_id] | |
| if len(ids) <= max_len: | |
| break | |
| else: | |
| return None | |
| names = [t.name for t in ex.tools] | |
| dec_labels, len_items = [], [] | |
| for i in range(MAX_CALLS): | |
| if i < len(ex.calls): | |
| c = ex.calls[i] | |
| if c.name not in names: | |
| return None | |
| j = names.index(c.name) | |
| dec_labels.append(j) | |
| for k, p in enumerate(ex.tools[j].params): | |
| b = bucket_of(len(value_tokens(tok, c.args[p.name]))) if p.name in c.args else 0 | |
| len_items.append((i, j, k, b)) | |
| else: | |
| dec_labels.append(len(ex.tools)) # NONE | |
| if len(ex.calls) > MAX_CALLS: | |
| dec_labels = [-100] * MAX_CALLS # can't represent; train nothing on decisions | |
| len_items = [it for it in len_items if it[0] < MAX_CALLS] | |
| return {"input_ids": ids, "t_pos": t_pos, "call_pos": call_pos, "arg_pos": arg_pos, | |
| "dec_labels": dec_labels, "len_items": len_items, "desc_words": dw} | |
| def build_pass_b(tok, ex: Example, a: dict, plan: list[tuple[int, dict[int, int]]], | |
| with_targets: bool, max_len: int = MAX_LEN_B, keep_context: bool = False, | |
| markers: bool = False) -> dict | None: | |
| """Append the slot block for `plan` = [(tool_j, {param_k: bucket})] per call. | |
| Buckets 1..4 get BUCKETS[b] mask slots; 0 (absent) and 5 (overflow) get none.""" | |
| # keep_context=True (cached variant): the full pass-A sequence is the prefix, so its states can be reused | |
| ids = (a["input_ids"] if keep_context else a["input_ids"][:-1]) + [tok.am["[ARGS]"]] | |
| ctx_len = len(a["input_ids"]) if keep_context else len(ids) - 1 | |
| slots, targets, index = [], [], [] | |
| for i, (j, buckets) in enumerate(plan): | |
| tool = ex.tools[j] | |
| # markers=True (v2): '#1 tool(' '#2 tool(' so repeated calls of the same tool are distinguishable | |
| ids += _enc(tok, (f" #{i + 1} " if markers else "") + tool.name + "(") | |
| first = True | |
| for k, p in enumerate(tool.params): | |
| b = buckets.get(k, 0) | |
| if b == 0 or b == OVERFLOW: | |
| continue | |
| ids += _enc(tok, ("" if first else ", ") + p.name + "=") | |
| first = False | |
| n = BUCKETS[b] | |
| pos = list(range(len(ids), len(ids) + n)) | |
| ids += [tok.mask_token_id] * n | |
| slots.append(pos) | |
| index.append((i, k)) | |
| if with_targets: | |
| v = value_tokens(tok, ex.calls[i].args[p.name])[:n] | |
| targets.append(v + [tok.pad_token_id] * (n - len(v))) | |
| ids += _enc(tok, ")") | |
| ids.append(tok.sep_token_id) | |
| if len(ids) > max_len: | |
| return None | |
| out = {"input_ids": ids, "slot_pos": slots, "slot_index": index, "ctx_len": ctx_len} | |
| if with_targets: | |
| out["slot_targets"] = targets | |
| return out | |
| def gold_plan(a: dict, ex: Example) -> list[tuple[int, dict[int, int]]]: | |
| """One (tool_j, {param_k: bucket}) entry per gold call, in call order - including calls to tools | |
| with no parameters (an empty dict), so plan index == call index.""" | |
| plan = [] | |
| for i in range(min(len(ex.calls), MAX_CALLS)): | |
| buckets = {k: b for (ci, _, k, b) in a["len_items"] if ci == i} | |
| plan.append((a["dec_labels"][i], buckets)) | |
| return plan | |