Download code/actionman/schema.py from shing-dev/ActionMan-0.1: direct link, hf CLI and curl.
- Browser
- Download file 9.26 kB
-
https://huggingface.co/shing-dev/ActionMan-0.1/resolve/main/code/actionman/schema.py
- Command line
-
hf download hf://shing-dev/ActionMan-0.1/code/actionman/schema.py
-
curl -L -o schema.py https://huggingface.co/shing-dev/ActionMan-0.1/resolve/main/code/actionman/schema.py
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") | |
| 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) | |
| class Tool: | |
| name: str | |
| description: str = "" | |
| params: list[Param] = field(default_factory=list) | |
| class Call: | |
| name: str | |
| args: dict[str, Any] = field(default_factory=dict) | |
| 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", [])) | |