PyTorch
Safetensors
action-model
function-calling
tool-calling
tool-use
non-autoregressive
masked-diffusion
modernbert
shing-dev's picture
ActionMan-0.1 research preview
6fd3740 verified
Raw History Blame Contribute Delete
9.26 kB
"""Unified tool/call schema, converters from xLAM and BFCL, compact signatures, value <-> text."""
from __future__ import annotations
import ast
import json
from dataclasses import dataclass, field
from typing import Any
TYPES = ("str", "int", "float", "bool", "list", "dict", "any")
@dataclass
class Param:
name: str
type: str = "any"
optional: bool = False
default: Any = None
description: str = ""
options: list | None = None # declared enum values (v3 option slots)
@dataclass
class Tool:
name: str
description: str = ""
params: list[Param] = field(default_factory=list)
@dataclass
class Call:
name: str
args: dict[str, Any] = field(default_factory=dict)
@dataclass
class Example:
query: str
tools: list[Tool]
calls: list[Call]
id: str = ""
source: str = ""
# gold in BFCL "possible answer" form: [{func: {arg: [acceptable values]}}]; "" = may be omitted
gold: list[dict] = field(default_factory=list)
# ---------- type normalisation ----------
def norm_type(t: Any) -> tuple[str, bool]:
"""Map xLAM ('str, optional', 'List[int]') or JSON-schema ('integer') types to (TYPE, optional)."""
s = str(t or "any").strip()
low = s.lower()
optional = "optional" in low
head = low.split(",")[0].split("[")[0].strip()
table = {
"str": "str", "string": "str",
"int": "int", "integer": "int",
"float": "float", "number": "float", "double": "float",
"bool": "bool", "boolean": "bool",
"list": "list", "array": "list", "tuple": "list", "set": "list",
"dict": "dict", "object": "dict",
}
return table.get(head, "any"), optional
# ---------- converters ----------
def from_xlam(row: dict) -> Example:
tools_raw = json.loads(row["tools"]) if isinstance(row["tools"], str) else row["tools"]
answers = json.loads(row["answers"]) if isinstance(row["answers"], str) else row["answers"]
tools = []
for t in tools_raw:
params = []
for pname, spec in (t.get("parameters") or {}).items():
spec = spec or {}
ty, opt = norm_type(spec.get("type"))
default = spec.get("default")
if default == "":
default = None
params.append(Param(pname, ty, opt, default, spec.get("description", "") or ""))
tools.append(Tool(t["name"], t.get("description", "") or "", params))
calls = [Call(a["name"], dict(a.get("arguments") or {})) for a in answers]
ex = Example(row["query"], tools, calls, id=f"xlam_{row.get('id', '')}", source="xlam")
ex.gold = gold_from_calls(tools, calls)
return ex
def from_bfcl(q_row: dict, answer_row: dict | None, category: str) -> Example:
query = q_row["question"][0][-1]["content"] if q_row["question"] and q_row["question"][0] else ""
tools = []
for f in q_row["function"]:
props = (f.get("parameters") or {}).get("properties", {}) or {}
required = set((f.get("parameters") or {}).get("required", []) or [])
params = []
for pname, spec in props.items():
ty, _ = norm_type(spec.get("type"))
params.append(Param(pname, ty, pname not in required, spec.get("default"), spec.get("description", "") or "",
spec.get("enum") or None))
tools.append(Tool(f["name"], f.get("description", "") or "", params))
gold = answer_row["ground_truth"] if answer_row else []
calls = []
for g in gold:
(name, args), = g.items()
calls.append(Call(name, {k: v[0] for k, v in args.items() if v and v[0] != ""}))
return Example(query, tools, calls, id=q_row["id"], source=f"bfcl_{category}", gold=gold)
def gold_from_calls(tools: list[Tool], calls: list[Call]) -> list[dict]:
"""Build BFCL-style acceptable-value gold from exact xLAM answers."""
by_name = {t.name: t for t in tools}
gold = []
for c in calls:
accept: dict[str, list] = {k: [v] for k, v in c.args.items()}
tool = by_name.get(c.name)
if tool:
for p in tool.params:
if p.name in accept:
if p.optional and p.default is not None and _same(accept[p.name][0], p.default):
accept[p.name].append("")
elif p.optional or p.default is not None:
accept[p.name] = [""] + ([p.default] if p.default is not None else [])
gold.append({c.name: accept})
return gold
def _same(a: Any, b: Any) -> bool:
try:
return a == b or float(a) == float(b)
except (TypeError, ValueError):
return str(a) == str(b)
MAX_OPTIONS = 16
def param_options(p: Param) -> list | None:
"""Values a v3 option slot picks from: bools -> [True, False], declared enums of <= MAX_OPTIONS, else None."""
if p.type == "bool":
return [True, False]
if p.options and len(p.options) <= MAX_OPTIONS:
return list(p.options)
return None
# ---------- compact signature ----------
def _trim(text: str, words: int) -> str:
w = " ".join(str(text).split()).split(" ")
return " ".join(w[:words]) if words > 0 else ""
def signature_pieces(tool: Tool, desc_words: int = 8) -> list[tuple[str, str, int]]:
"""Pieces of `name(p:type=default "desc", q?:int) | description`.
Each piece is (text, kind, param_index); kind in {'name', 'param', 'text'}.
The 'param' piece is exactly the parameter name so its first token can be located."""
pieces: list[tuple[str, str, int]] = [(tool.name, "name", -1), ("(", "text", -1)]
for k, p in enumerate(tool.params):
if k:
pieces.append((", ", "text", -1))
pieces.append((p.name, "param", k))
spec = ("?" if p.optional else "") + ":" + p.type
if p.default is not None:
spec += "=" + value_to_text(p.default)
d = _trim(p.description, desc_words)
if d:
spec += f' "{d}"'
pieces.append((spec, "text", -1))
pieces.append((")", "text", -1))
d = _trim(tool.description, max(desc_words + 4, 0) if desc_words > 0 else 0)
if d:
pieces.append((" | " + d, "text", -1))
return pieces
def signature(tool: Tool, desc_words: int = 8) -> str:
return "".join(t for t, _, _ in signature_pieces(tool, desc_words))
# ---------- values ----------
def value_to_text(v: Any) -> str:
if isinstance(v, str):
return v
if isinstance(v, bool):
return "true" if v else "false"
return json.dumps(v, ensure_ascii=False)
def text_to_value(s: str, ty: str) -> Any:
s = s.strip()
if ty == "str":
return s
if ty == "bool":
return s.lower() in ("true", "1", "yes")
if ty in ("int", "float"):
try:
f = float(s)
if ty == "int" and f.is_integer():
return int(f)
return f
except ValueError:
pass
for parse in (json.loads, ast.literal_eval):
try:
return parse(s)
except Exception:
continue
return s
# ---------- OpenAI-style tool (for the LLM baseline) ----------
_JSON_TYPES = {"str": "string", "int": "integer", "float": "number", "bool": "boolean",
"list": "array", "dict": "object", "any": "string"}
def from_openai_tool(t: dict) -> Tool:
"""OpenAI-style tool ({"type": "function", "function": {...}} or the bare function dict) -> Tool."""
f = t.get("function", t)
ps = f.get("parameters") or {}
req = set(ps.get("required", []))
params = []
for name, s in (ps.get("properties") or {}).items():
ty, _ = norm_type(s.get("type"))
params.append(Param(name, ty, name not in req, s.get("default"), s.get("description", "") or "", s.get("enum")))
return Tool(f["name"], f.get("description", "") or "", params)
def to_openai_tool(tool: Tool) -> dict:
props = {}
for p in tool.params:
spec = {"type": _JSON_TYPES[p.type], "description": p.description}
if p.default is not None:
spec["default"] = p.default
props[p.name] = spec
return {"type": "function", "function": {
"name": tool.name, "description": tool.description,
"parameters": {"type": "object", "properties": props,
"required": [p.name for p in tool.params if not p.optional]}}}
# ---------- (de)serialisation for jsonl ----------
def example_to_dict(ex: Example) -> dict:
return {"id": ex.id, "source": ex.source, "query": ex.query, "gold": ex.gold,
"tools": [{"name": t.name, "description": t.description,
"params": [vars(p) for p in t.params]} for t in ex.tools],
"calls": [vars(c) for c in ex.calls]}
def example_from_dict(d: dict) -> Example:
tools = [Tool(t["name"], t["description"], [Param(**p) for p in t["params"]]) for t in d["tools"]]
return Example(d["query"], tools, [Call(**c) for c in d["calls"]], d["id"], d["source"], d.get("gold", []))