Files
wehub-resource-sync 426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

3961 lines
156 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Per-source-format adapters for the normalizer.
Every adapter yields the DEPRECATED flat `ElizaRecord` shape (see
`scripts/lib/eliza_record.py`). That intermediate is converged to the rendered
ChatML training example by `scripts/format_for_training.py`. It is NOT the
canonical Eliza-1 corpus record — that is `eliza_native_v1`; see
`packages/training/docs/dataset/CANONICAL_RECORD.md`. No new adapter should be
added here; new corpus data should be authored as `eliza_native_v1` rows.
Current canonical action vocabulary (used in `availableActions`; mirror
`packages/core/src/generated/action-docs.ts`):
RESPOND, IGNORE, STOP — shouldRespond decision values (not actions)
REPLY — emit a reply (similes RESPOND/RESPONSE/GREET)
SHELL — execute a shell command
TASKS — orchestrator action (spawn / send / stop / history / share / call)
USE_SKILL / SKILL — invoke an enabled skill / skill-catalog ops
APP, GENERATE_MEDIA, CHOOSE_OPTION, ... — plus per-tool / per-skill custom names
The supervised target is `expectedResponse` (a JSON planner document for
structured tasks, plain text for replies).
"""
from __future__ import annotations
import hashlib
import json
import logging
import re
from typing import Any, Callable, Iterator
from .eliza_record import (
ACTION_IGNORE,
ACTION_REPLY,
ACTION_RESPOND,
ACTION_SHELL,
ACTION_TASKS,
DEFAULT_THOUGHT_LEAKS,
ElizaRecord,
REPLY_ACTIONS,
ROUTING_ACTIONS,
build,
is_default_thought_leak,
stable_id,
)
from .expected_response import ExpectedResponseEncoder
log = logging.getLogger("adapter")
# Regression guard: the literal default-thought strings the legacy adapters
# injected as a fallback `thought` whenever the upstream record lacked a real
# reasoning trace are forbidden from re-entering this module as defaults.
# DEFAULT_THOUGHT_LEAKS is the canonical leak list (see lib/eliza_record.py);
# this assertion fails fast at import time if anyone re-introduces them as
# adapter-default constants.
assert "Reply to the user." in DEFAULT_THOUGHT_LEAKS
assert "Call the tool to satisfy the request." in DEFAULT_THOUGHT_LEAKS
# Every adapter has the same call signature. `records` is whatever
# `normalize.py:load_records()` yields — JSONL/JSON/parquet rows decoded
# into dicts; the `_source_filename` injection lets file-aware adapters
# pick a task_type per shard. Adapters yield canonical `ElizaRecord`s.
Adapter = Callable[..., Iterator[ElizaRecord]]
ROLE_MAP = {
"user": "user", "human": "user", "USER": "user", "question": "user",
"assistant": "assistant", "gpt": "assistant", "model": "assistant",
"ai": "assistant", "ASSISTANT": "assistant",
"answer": "assistant", "response": "assistant", "agent": "assistant",
"bot": "assistant",
"system": "system", "SYSTEM": "system", "developer": "system",
"tool": "tool", "function": "tool", "tool_response": "tool",
"observation": "tool", "tool_result": "tool", "function_response": "tool",
"tool call": "assistant", "tool_call": "assistant",
# Some sources (regularizer-reasoning-tool) ship a separate
# "reasoning" role that PRECEDES the corresponding assistant turn.
# We tag it explicitly here so `_split_history` can attach it as the
# `thought` of the next assistant turn rather than dropping it.
"reasoning": "reasoning", "thought": "reasoning",
"analysis": "reasoning",
}
def _norm_role(r: str) -> str:
if not r:
return "user"
return ROLE_MAP.get(r, ROLE_MAP.get(r.lower(), r.lower()))
def _strip_surrogates(s: str) -> str:
"""Remove unpaired surrogate codepoints. Some upstream JSON (notably
agent-trove parquet shards from terminus-2 traces) contains lone
`\\udcca` bytes — these survive the parquet decode but break the
bun encoder's `stdin.write` because Python's UTF-8 encoder rejects
surrogates. Replacing is safe: these are byte-level garbage from
the upstream, never meaningful glyphs."""
if not isinstance(s, str):
return s
return s.encode("utf-8", "replace").decode("utf-8", "replace")
def _split_history(messages: list[dict[str, Any]]) -> tuple[
str, list[dict[str, Any]], dict[str, Any] | None, dict[str, Any] | None
]:
"""Return (system_prompt, memoryEntries, currentMessage, finalAssistant).
The first system turn(s) collapse into `system_prompt` (returned
separately so adapters can stash it under metadata). The last assistant
turn becomes the supervised target. The last user turn before that
becomes `currentMessage`.
"""
system_parts: list[str] = []
convo: list[dict[str, Any]] = []
for m in messages:
# Defensive: some sources mix in plain-string entries inside the
# messages list (e.g. open-paws-tool-use, toucan, regularizer).
# Treat a bare string as a user turn so we don't lose the record.
if isinstance(m, str):
m = {"role": "user", "content": m}
elif not isinstance(m, dict):
continue
role = _norm_role(m.get("role") or m.get("from") or "")
content = m.get("content") if "content" in m else m.get("value")
# Keep an assistant turn even when content is null IF it carries
# tool_calls — that's how OpenAI ships function-only assistant turns
# (e.g. google/mobile-actions). _extract_tool_calls reads from raw.
if content is None:
if role == "assistant" and (m.get("tool_calls") or m.get("function_call")):
content = ""
else:
continue
if isinstance(content, list):
content = "".join(
p.get("text", "") if isinstance(p, dict) else str(p)
for p in content
)
if role == "system":
system_parts.append(str(content))
continue
if role == "reasoning":
# Hold this thought; attach to the next assistant turn we see.
# Strip <think> wrappers if the upstream source still has them.
txt = str(content).strip()
mt = re.match(r"<think>([\s\S]*?)</think>\s*", txt)
if mt:
txt = mt.group(1).strip()
convo.append({"role": "reasoning", "content": txt, "raw": m})
continue
entry: dict[str, Any] = {"role": role, "content": str(content), "raw": m}
# Some sources ship a sibling reasoning/thinking field on the
# assistant message itself (opus-46-10kx-bas95: `reasoning`;
# talos-kimi/Kimi-style traces: `thinking`; a few qwen3 dumps
# use `thought`). Capture it so we can populate `thought:` later.
if role == "assistant":
for key in ("reasoning", "thinking", "thought", "reasoning_content"):
v = m.get(key)
if isinstance(v, str) and v.strip():
entry["_pending_thought"] = v.strip()
break
convo.append(entry)
# Coalesce any pending reasoning-role messages into the *next* assistant
# turn's `_pending_thought` field, then drop the reasoning entries from
# the conversation. This keeps the standard memory/history clean while
# preserving the upstream reasoning so we can use it as `thought:`.
coalesced: list[dict[str, Any]] = []
pending_thoughts: list[str] = []
for m in convo:
if m["role"] == "reasoning":
if m["content"]:
pending_thoughts.append(m["content"])
continue
if m["role"] == "assistant" and pending_thoughts:
existing = m.get("_pending_thought") or ""
joined = "\n\n".join([t for t in [existing, *pending_thoughts] if t]).strip()
m = {**m, "_pending_thought": joined}
pending_thoughts = []
coalesced.append(m)
convo = coalesced
# Find the last assistant turn anywhere in the convo (not just at the
# tail). Agent traces (swebench, hf-coding-tools) often end on a user
# `tool_output` turn — we still want to train on the previous assistant
# action that PRECEDED it.
final_assistant: dict[str, Any] | None = None
final_idx = -1
for i in range(len(convo) - 1, -1, -1):
if convo[i]["role"] == "assistant":
final_assistant = convo[i]
final_idx = i
break
if final_assistant is not None:
# Drop the final assistant turn AND anything after it (subsequent
# user/tool turns aren't part of this training record).
convo = convo[:final_idx]
current_msg: dict[str, Any] | None = None
for m in reversed(convo):
if m["role"] == "user":
current_msg = {
"role": "user",
"speaker": "user",
"content": m["content"],
"channel": "dm",
}
convo.remove(m)
break
memory = [
{
"role": m["role"],
"speaker": m["role"],
"content": m["content"],
"channel": "dm",
}
for m in convo
]
return "\n\n".join(system_parts), memory, current_msg, final_assistant
def _extract_tool_calls(
assistant: dict[str, Any]
) -> list[dict[str, Any]]:
"""Pull tool calls from an assistant turn.
Recognized formats (in order):
1. OpenAI ``tool_calls`` array on the raw message.
2. OpenAI legacy ``function_call`` object on the raw message.
3. JSON content with ``tool_calls`` / ``toolCalls`` fields.
"""
raw = assistant.get("raw") or {}
content = assistant.get("content") or ""
def normalize_one(tc: Any) -> dict[str, Any] | None:
if not isinstance(tc, dict):
return None
fn = tc.get("function") or {}
if not isinstance(fn, dict):
fn = {}
args = (
tc.get("arguments")
if "arguments" in tc
else tc.get("args")
if "args" in tc
else tc.get("input")
if "input" in tc
else fn.get("arguments")
)
if isinstance(args, str):
try:
args = json.loads(args)
except json.JSONDecodeError:
pass
name = tc.get("name") or tc.get("tool_name") or tc.get("toolName") or fn.get("name")
if not isinstance(name, str) or not name.strip():
return None
return {"name": name.strip(), "arguments": args if isinstance(args, dict) else {}}
# OpenAI-format: assistant.tool_calls = [{id,type,function:{name,arguments}}]
# Some sources (playwright-mcp-toolcalling/train_v4) ship the array
# as a stringified JSON blob — decode if so.
raw_calls = raw.get("tool_calls")
if isinstance(raw_calls, str):
try:
raw_calls = json.loads(raw_calls)
except json.JSONDecodeError:
raw_calls = []
parsed: list[dict[str, Any]] = []
for tc in (raw_calls or []):
normalized = normalize_one(tc)
if normalized:
parsed.append(normalized)
if not parsed and isinstance(raw.get("function_call"), dict):
normalized = normalize_one({"function": raw["function_call"]})
if normalized:
parsed.append(normalized)
if not parsed and isinstance(content, str):
body = content.strip()
if body.startswith("{") and body.endswith("}"):
try:
obj = json.loads(body)
except json.JSONDecodeError:
obj = {}
if isinstance(obj, dict):
calls = obj.get("tool_calls") or obj.get("toolCalls")
if isinstance(calls, list):
parsed.extend(
normalized for call in calls
if (normalized := normalize_one(call))
)
else:
normalized = normalize_one(obj)
if normalized:
parsed.append(normalized)
return parsed
_THINK_RE = re.compile(r"<think>([\s\S]*?)</think>\s*", re.M)
_THINKING_RE = re.compile(r"<thinking>([\s\S]*?)</thinking>\s*", re.M)
_THOUGHT_PREFIX_RE = re.compile(
r"^\s*THOUGHT:\s*([\s\S]*?)(?=\n\s*```|\Z)", re.M
)
def _split_think_response(text: str) -> tuple[str, str]:
"""Return (reasoning, final_response) from a `<think>…</think>\\nfinal`
blob. If no <think> block is present, reasoning="" and the whole text
is the response.
Also recognizes `<thinking>...</thinking>` and the swebench-style
`THOUGHT: ...` prefix that precedes a fenced
bash block.
"""
if not text:
return "", ""
m = _THINK_RE.match(text)
if m:
return m.group(1).strip(), text[m.end():].strip()
m = _THINKING_RE.match(text)
if m:
return m.group(1).strip(), text[m.end():].strip()
m = _THOUGHT_PREFIX_RE.match(text)
if m:
thought = m.group(1).strip()
if thought:
rest = text[m.end():].lstrip("\n")
# Only treat as a real THOUGHT prefix when followed by a
# bash/code block — avoids false matches on prose that
# happens to start with the word "THOUGHT:".
if rest.startswith("```"):
return thought, rest.strip()
return "", text.strip()
def _extract_agent_trove_json_thought(text: str) -> tuple[str, str]:
"""Detect the agent-trove / nemotron-terminal JSON envelope:
`{"analysis": ..., "plan": ..., "commands": [...], "task_complete": bool}`.
When matched, return (thought, text) where:
- thought = analysis + plan (newline-joined)
- text = the original JSON unchanged (the model still needs to
emit the full structured output for the runtime).
When the body is not this shape, return ("", text).
"""
body = text.strip()
if not (body.startswith("{") and body.endswith("}")):
return "", text
try:
obj = json.loads(body)
except (json.JSONDecodeError, ValueError):
return "", text
if not isinstance(obj, dict):
return "", text
analysis = obj.get("analysis")
plan = obj.get("plan")
parts: list[str] = []
if isinstance(analysis, str) and analysis.strip():
parts.append(analysis.strip())
if isinstance(plan, str) and plan.strip():
parts.append("Plan: " + plan.strip())
if not parts:
return "", text
return "\n\n".join(parts), text
def _split_thought_and_body(text: str) -> tuple[str, str]:
"""Combine all known reasoning-extraction strategies.
Returns (thought, body). If nothing matches, returns ("", text.strip()).
"""
if not text:
return "", ""
thought, rest = _split_think_response(text)
if thought:
return thought, rest
# JSON envelope ({analysis, plan, commands}) — keep the original text
# because the structured payload IS the action the model must emit;
# we just lift `analysis` + `plan` into `thought` for the planner.
thought, _ = _extract_agent_trove_json_thought(text)
if thought:
return thought, text.strip()
return "", text.strip()
def _cot_to_expected(
encoder: ExpectedResponseEncoder,
raw_text: str,
*,
extra_thought: str = "",
) -> str:
"""Wrap a raw chain-of-thought reply as the configured target format.
Produces `{thought, text}` when a reasoning block can be extracted from
`<think>` / `<thinking>` / `THOUGHT:` markers in the body,
OR when `extra_thought` is supplied. Native v5 encodes that object as JSON.
"""
thought, body = _split_thought_and_body(raw_text or "")
if extra_thought:
thought = (extra_thought.strip() + ("\n\n" + thought if thought else "")).strip()
body = _strip_surrogates(body)
thought = _strip_surrogates(thought)
if thought:
return encoder.encode({"thought": thought, "text": body})
return encoder.encode({"text": body})
# ─────────────────────── canonical planner envelope ─────────────────────────
# Task types whose canonical target IS the full 5-key planner envelope even
# when the assistant turn is a plain free-text reply (PIPELINE_SCHEMAS.md §1).
# `reply` and `reasoning_cot` are intentionally NOT in this set — they map to
# the slim replyTemplate / thinkTemplate forms (`{thought, text}` / `{text}`).
_PLANNER_REPLY_TASK_TYPES = frozenset({
"agent_trace",
"mobile_action",
"shell_command",
"n8n_workflow_generation",
})
# Pool of thought-phrasings used when the source corpus carries no reasoning
# trace. The previous implementation used a single literal string per action
# class (e.g. `"Reply to the user."`), which trained the model to emit that
# exact string verbatim — surveys of the 7M-record corpus showed 100% of
# Hermes-family records had `"Reply to the user."` as the model's "thought",
# teaching the production model to copy it verbatim instead of reasoning.
#
# We now hash the user message + action name to pick from a phrasing pool, so
# the same upstream record always gets the same thought (deterministic) but
# the corpus distribution is varied. Pools are intentionally short and
# stylistically diverse so no single phrasing dominates.
_REPLY_THOUGHT_POOL = (
"User asked a direct question; answering.",
"Drafting a reply.",
"Composing a response.",
"Replying with the requested information.",
"Returning the answer the user expects.",
"Formulating a reply to the message.",
"Writing back what the user needs.",
"Acknowledging and answering.",
"Producing the requested output.",
"Engaging with the user's request.",
)
_TOOL_THOUGHT_POOL = (
"Need a tool to satisfy this — picking the right one.",
"Routing to a tool call.",
"Tool needed; selecting the matching one.",
"Dispatching to a tool.",
"Invoking the relevant tool.",
"Calling out to a tool to gather what's needed.",
"Identifying the required tool.",
"Function call required for this request.",
"Reaching for a tool to handle this.",
"Tool dispatch in order.",
)
_SHELL_THOUGHT_POOL = (
"Need a shell command to do this.",
"Running a shell command.",
"Dispatching a shell call.",
"Shell command needed for this step.",
"Executing in the shell.",
"Reaching for a shell call.",
"Running this in the terminal.",
"Shell action is the right move here.",
"Command needed; running it.",
"Issuing a terminal command.",
)
_IGNORE_THOUGHT_POOL = (
"Not addressed to me; staying quiet.",
"Off-topic for this room — ignoring.",
"No engagement warranted.",
"Skipping this turn.",
"This isn't a request to respond to.",
"Nothing to act on here.",
"Holding back — not for me.",
"Letting this pass.",
"Not the kind of message I should reply to.",
"Standing down on this turn.",
)
_AGENT_TRACE_THOUGHT_POOL = (
"Continuing the running task.",
"Next step in the trajectory.",
"Pushing the task forward.",
"Advancing the active goal.",
"Handling the next planned step.",
"Carrying on with the work.",
"Moving to the next step.",
"Continuing what was started.",
"Working through the task.",
"Proceeding with the agent loop.",
)
def _picked_thought(pool: tuple[str, ...], seed: str) -> str:
"""Pick a phrasing from the pool deterministically based on a content seed.
Same input → same thought, but the corpus distribution rotates through
the pool, eliminating the single-string monoculture problem.
Uses sha256 (NOT Python's `hash()`) because `hash()` is randomized per
process (PYTHONHASHSEED), which would make the same upstream record
produce a different thought on every run — defeating the determinism
contract every downstream tool depends on.
"""
if not seed:
return pool[0]
digest = hashlib.sha256(seed[:256].encode("utf-8", "replace")).digest()
h = int.from_bytes(digest[:8], "big")
return pool[h % len(pool)]
# Backward-compatible wrappers — callers pass a seed (typically the user
# message) and get a varied phrasing. Falls back to the first pool entry
# when no seed is provided.
def _DEFAULT_REPLY_THOUGHT_for(seed: str = "") -> str:
return _picked_thought(_REPLY_THOUGHT_POOL, seed)
def _DEFAULT_TOOL_THOUGHT_for(seed: str = "") -> str:
return _picked_thought(_TOOL_THOUGHT_POOL, seed)
def _DEFAULT_SHELL_THOUGHT_for(seed: str = "") -> str:
return _picked_thought(_SHELL_THOUGHT_POOL, seed)
def _DEFAULT_IGNORE_THOUGHT_for(seed: str = "") -> str:
return _picked_thought(_IGNORE_THOUGHT_POOL, seed)
def _DEFAULT_AGENT_TRACE_THOUGHT_for(seed: str = "") -> str:
return _picked_thought(_AGENT_TRACE_THOUGHT_POOL, seed)
# Compat aliases — keep the old names so existing call-sites still work,
# but they now resolve to first pool entry. New call-sites should use
# the `_for(seed)` helpers when a content seed is available.
_DEFAULT_REPLY_THOUGHT = _REPLY_THOUGHT_POOL[0]
_DEFAULT_TOOL_THOUGHT = _TOOL_THOUGHT_POOL[0]
_DEFAULT_SHELL_THOUGHT = _SHELL_THOUGHT_POOL[0]
_DEFAULT_IGNORE_THOUGHT = _IGNORE_THOUGHT_POOL[0]
_DEFAULT_AGENT_TRACE_THOUGHT = _AGENT_TRACE_THOUGHT_POOL[0]
def _planner_envelope(
*,
thought: str,
actions: list[Any],
providers: list[str] | None = None,
text: str = "",
simple: bool = True,
seed: str = "",
) -> dict[str, Any]:
"""Build the canonical 5-key planner envelope dict.
The runtime parser (`message.ts:5616-5657`) reads exactly these five keys:
`thought`, `actions`, `providers`, `text`, `simple`. Each `actions[]`
entry is either a bare uppercase action-name string OR an object
`{name, params?}`.
All strings flow through `_strip_surrogates` so the bun encoder accepts
them. Providers default to an empty list. `simple` defaults to True so
the planner says "send `text` directly" — callers that want
action-driven finalization (e.g. when REPLY runs as the action) MUST
pass `simple=False`.
`seed` is retained for back-compat but is no longer used to synthesize
a default thought — when the upstream record carries no real reasoning
trace, the `thought` field is OMITTED from the envelope entirely. The
runtime planner parser tolerates a missing `thought:` key, and the
student model is therefore not trained to emit a placeholder phrase.
"""
del seed # back-compat only; default-thought synthesis is removed
raw_thought = _strip_surrogates(thought or "").strip()
# Defense in depth: if any upstream caller smuggles in one of the
# canonical leak literals (or wraps it in quotes), treat it as if no
# thought was provided and drop the field. The literals are defined
# once in `lib/eliza_record.DEFAULT_THOUGHT_LEAKS`.
if is_default_thought_leak(raw_thought):
raw_thought = ""
safe_text = _strip_surrogates(text or "")
safe_actions: list[Any] = []
for a in actions:
if isinstance(a, str):
up = a.strip().upper()
if up:
safe_actions.append(up)
continue
if isinstance(a, dict):
name = str(a.get("name", "")).strip().upper()
if not name:
continue
params = a.get("params")
if isinstance(params, dict) and params:
safe_actions.append({"name": name, "params": params})
else:
safe_actions.append({"name": name})
safe_providers = [str(p) for p in (providers or []) if isinstance(p, str)]
envelope: dict[str, Any] = {
"actions": safe_actions,
"providers": safe_providers,
"text": safe_text,
"simple": bool(simple),
}
if raw_thought:
# Insert at the head so encoded structured targets keep canonical key order.
envelope = {"thought": raw_thought, **envelope}
return envelope
def _planner_reply_envelope(
*, thought: str, text: str, providers: list[str] | None = None,
seed: str = "",
) -> dict[str, Any]:
"""Planner envelope for a plain REPLY action.
`simple=true` — the planner's `text` IS the final reply (no need to
re-run REPLY to generate text).
If the upstream record carries no real `thought`, the field is omitted
from the envelope rather than synthesized. The runtime planner parser
tolerates a missing `thought:` line.
"""
if is_default_thought_leak(thought):
thought = ""
del seed # retained for back-compat; default-thought synthesis is removed
return _planner_envelope(
thought=thought,
actions=["REPLY"],
providers=providers or [],
text=text,
simple=True,
)
def _planner_tool_envelope(
*,
thought: str,
tool_calls: list[dict[str, Any]],
text: str = "",
providers: list[str] | None = None,
action_name: str = ACTION_TASKS,
) -> dict[str, Any]:
"""Planner envelope wrapping one or more tool calls.
Each `tool_calls` entry must be `{name, arguments}`. We emit one
`actions[]` entry per call with `params: {tool: <name>, arguments:
<arguments>}`. `simple=false` because actions drive the output.
Surrogate codepoints in tool names / argument values are stripped so
the encoder accepts the document.
"""
actions: list[dict[str, Any]] = []
for c in tool_calls:
if not isinstance(c, dict):
continue
name = str(c.get("name") or "").strip()
if not name:
continue
args = c.get("arguments")
if not isinstance(args, dict):
args = {}
actions.append({
"name": action_name,
"params": {
"tool": _strip_surrogates(name),
"arguments": args,
},
})
if not actions:
# Defensive: no callable tool found — fall back to a REPLY envelope
# so we never emit an empty `actions:` list (which the runtime would
# treat as the agent doing nothing).
return _planner_reply_envelope(
thought=thought,
text=text or "",
providers=providers or [],
)
if is_default_thought_leak(thought):
thought = ""
return _planner_envelope(
thought=thought,
actions=actions,
providers=providers or [],
text=text or "",
simple=False,
)
def _planner_shell_envelope(
*,
thought: str,
command: str,
explanation: str = "",
cwd: str = "",
text: str = "",
providers: list[str] | None = None,
) -> dict[str, Any]:
"""Planner envelope for a SHELL action.
The shell-action params surface as `{command, [cwd], [explanation]}`.
"""
params: dict[str, Any] = {"command": _strip_surrogates(command)}
if cwd:
params["cwd"] = _strip_surrogates(cwd)
if explanation:
params["explanation"] = _strip_surrogates(explanation)
if is_default_thought_leak(thought):
thought = ""
return _planner_envelope(
thought=thought,
actions=[{"name": ACTION_SHELL, "params": params}],
providers=providers or [],
text=text or "",
simple=False,
)
def _planner_ignore_envelope(
*, thought: str, text: str = "", seed: str = "",
) -> dict[str, Any]:
"""Planner envelope for an IGNORE decision (no reply, no actions).
If the upstream record carries no real `thought`, the field is omitted
from the envelope rather than synthesized.
"""
if is_default_thought_leak(thought):
thought = ""
del seed # retained for back-compat; default-thought synthesis is removed
return _planner_envelope(
thought=thought,
actions=["IGNORE"],
providers=[],
text=text or "",
simple=True,
)
def _normalize_tools(tools_raw: Any) -> list[dict[str, Any]]:
if isinstance(tools_raw, str):
try:
tools_raw = json.loads(tools_raw)
except json.JSONDecodeError:
return []
if not isinstance(tools_raw, list):
return []
out: list[dict[str, Any]] = []
for t in tools_raw:
if not isinstance(t, dict):
continue
if "function" in t and isinstance(t["function"], dict):
fn = t["function"]
out.append({
"name": fn.get("name", ""),
"description": fn.get("description", ""),
"parameters": fn.get("parameters") or {},
})
else:
out.append({
"name": t.get("name", ""),
"description": t.get("description", ""),
"parameters": t.get("parameters") or {},
})
return out
def _build_messages_record(
*, slug: str, license: str, split: str,
sys_prompt: str, memory: list[dict[str, Any]],
current: dict[str, Any], assistant: dict[str, Any],
encoder: ExpectedResponseEncoder,
tools_list: list[dict[str, Any]] | None = None,
default_task_type: str = "reply",
extra_metadata: dict[str, Any] | None = None,
room_seed: str | None = None,
) -> ElizaRecord:
"""Assemble a flat eliza record from already-split conversation parts."""
calls = _extract_tool_calls(assistant)
text = assistant.get("content", "") or ""
extra_thought = str(assistant.get("_pending_thought") or "")
thought, body = _split_thought_and_body(text)
if extra_thought:
thought = (extra_thought.strip() + ("\n\n" + thought if thought else "")).strip()
if calls:
# Tool / MCP call → planner envelope with TASKS action(s).
# PIPELINE_SCHEMAS.md §1+§5 — every tool_call record is wrapped in the
# planner 5-key document so the supervised target matches the runtime
# planner stage exactly.
task_type = "mcp_tool_call" if default_task_type == "mcp_tool_call" else "tool_call"
actions = [ACTION_TASKS, ACTION_REPLY, ACTION_IGNORE]
target = encoder.encode(_planner_tool_envelope(
thought=thought, tool_calls=calls, text=body, providers=[],
))
elif default_task_type in _PLANNER_REPLY_TASK_TYPES:
# Free-text reply on a planner-typed task (agent_trace, mobile_action,
# …) → full planner envelope with REPLY action so the schema audit
# passes (PIPELINE_SCHEMAS.md §1).
task_type = default_task_type
actions = REPLY_ACTIONS.copy()
target = encoder.encode(_planner_reply_envelope(
thought=thought, text=body, providers=[],
))
else:
# `reply` / `reasoning_cot` keep the slim `{thought, text}` /
# `{text}` form — that is the canonical replyTemplate /
# thinkTemplate output (PIPELINE_SCHEMAS.md §3-4).
# If the upstream source defaulted to a tool-call task but this
# conversation ended on free text, retag as `reply` so the
# task_type label matches the actual envelope shape.
if default_task_type in ("tool_call", "mcp_tool_call"):
task_type = "reply"
else:
task_type = default_task_type
actions = REPLY_ACTIONS.copy()
target = _cot_to_expected(encoder, text, extra_thought=extra_thought)
md = {
"original_id": str(extra_metadata.get("original_id", "") if extra_metadata else ""),
}
if sys_prompt:
md["system_prompt"] = sys_prompt
if tools_list:
md["toolSpecs"] = tools_list
if calls:
md["expected_tool_calls"] = calls
if extra_metadata:
md.update(extra_metadata)
# The flat ElizaRecord currentMessage carries one of {user, assistant}.
# When the supervised assistant turn is replying to a tool result,
# `_split_per_turn` hands us a `tool`-role `current`; surface that result
# as a user-side turn (which is exactly how format_for_training renders
# currentMessage anyway) so the row matches the runtime message model.
if current.get("role") not in ("user", "assistant"):
current = {**current, "role": "user", "speaker": "user"}
seed = room_seed or current["content"][:120]
return build(
roomName=stable_id(slug, seed),
agentId="agent",
memoryEntries=memory,
currentMessage=current,
expectedResponse=target,
availableActions=actions,
task_type=task_type,
source_dataset=slug,
license=license,
split=split,
extra_metadata=md,
)
def _generic_messages(
records: Iterator[dict], *, slug: str, license: str, split: str,
messages_key: str | Callable[[dict], list[dict]],
encoder: ExpectedResponseEncoder,
default_task_type: str = "reply",
tools_key: str | None = None,
) -> Iterator[ElizaRecord]:
"""Generic ShareGPT/OpenAI-messages adapter."""
for r in records:
msgs = (messages_key(r) if callable(messages_key) else r.get(messages_key)) or []
# Some sources (toucan, regularizer, nemotron-coding) ship `messages`
# as a stringified JSON array. JSON-decode and continue.
if isinstance(msgs, str):
s = msgs.strip()
if s.startswith("["):
try:
msgs = json.loads(s)
except json.JSONDecodeError:
continue
else:
# Llama-3 chat-template formatted text — skip; we don't
# currently parse <|start_header_id|> blobs back out.
continue
if not msgs:
continue
sys_prompt, memory, current, final = _split_history(msgs)
if not final or not current:
continue
tools_list = _normalize_tools(r.get(tools_key)) if tools_key else []
yield _build_messages_record(
slug=slug, license=license, split=split,
sys_prompt=sys_prompt, memory=memory, current=current,
assistant=final, encoder=encoder, tools_list=tools_list,
default_task_type=default_task_type,
extra_metadata={"original_id": str(r.get("id") or "")},
)
def _decode_message(m: Any) -> dict[str, Any] | None:
"""Decode one message entry into a canonical dict.
Some sources ship each message as a JSON-stringified blob inside the
list (e.g. playwright-mcp-toolcalling). Some legitimately ship dicts.
A bare string falls back to a user turn so we don't lose the row.
"""
if isinstance(m, str):
s = m.strip()
if s.startswith("{"):
try:
obj = json.loads(s)
if isinstance(obj, dict):
return obj
except json.JSONDecodeError:
pass
return {"role": "user", "content": m}
if isinstance(m, dict):
return m
return None
def _normalize_messages(msgs: Any) -> list[dict[str, Any]]:
"""Decode every entry in a messages list to a canonical dict."""
if not isinstance(msgs, list):
return []
out: list[dict[str, Any]] = []
for m in msgs:
d = _decode_message(m)
if d is not None:
out.append(d)
return out
def _split_per_turn(messages: list[dict[str, Any]]) -> tuple[
str, list[tuple[list[dict[str, Any]], dict[str, Any], dict[str, Any]]]
]:
"""Split a multi-turn trace into one supervised record per assistant turn.
Returns ``(system_prompt, [(memory, current, assistant), ...])`` where
each tuple is a self-contained training record. ``current`` is the
most recent user/tool turn before that assistant turn; ``memory`` is
everything before ``current``.
Only assistant turns that have content OR tool_calls are emitted.
"""
system_parts: list[str] = []
convo: list[dict[str, Any]] = []
for m in messages:
if not isinstance(m, dict):
continue
role = _norm_role(m.get("role") or m.get("from") or "")
content = m.get("content") if "content" in m else m.get("value")
if content is None:
if role == "assistant" and (m.get("tool_calls") or m.get("function_call")):
content = ""
else:
continue
if isinstance(content, list):
content = "".join(
p.get("text", "") if isinstance(p, dict) else str(p)
for p in content
)
if role == "system":
system_parts.append(str(content))
continue
if role == "reasoning":
txt = str(content).strip()
mt = re.match(r"<think>([\s\S]*?)</think>\s*", txt)
if mt:
txt = mt.group(1).strip()
convo.append({"role": "reasoning", "content": txt, "raw": m})
continue
entry: dict[str, Any] = {"role": role, "content": str(content), "raw": m}
if role == "assistant":
for key in ("reasoning", "thinking", "thought", "reasoning_content"):
v = m.get(key)
if isinstance(v, str) and v.strip():
entry["_pending_thought"] = v.strip()
break
convo.append(entry)
# Coalesce reasoning-role messages onto the next assistant turn.
coalesced: list[dict[str, Any]] = []
pending_thoughts: list[str] = []
for m in convo:
if m["role"] == "reasoning":
if m["content"]:
pending_thoughts.append(m["content"])
continue
if m["role"] == "assistant" and pending_thoughts:
existing = m.get("_pending_thought") or ""
joined = "\n\n".join([t for t in [existing, *pending_thoughts] if t]).strip()
m = {**m, "_pending_thought": joined}
pending_thoughts = []
coalesced.append(m)
convo = coalesced
sys_prompt = "\n\n".join(system_parts)
out: list[tuple[list[dict[str, Any]], dict[str, Any], dict[str, Any]]] = []
for i, m in enumerate(convo):
if m["role"] != "assistant":
continue
# Need a meaningful assistant turn: content (after strip) or
# tool_calls. ``tool_calls`` may legitimately be ``null`` on
# decoded JSON-message rows, so coerce explicitly.
raw_calls = m["raw"].get("tool_calls")
raw_fc = m["raw"].get("function_call")
has_calls = bool(raw_calls) or bool(raw_fc)
content_stripped = (m["content"] or "").strip()
if not content_stripped and not has_calls:
continue
# Find the most recent user (preferred) or tool turn before this
# assistant — that becomes ``current``. If neither exists, skip.
current_idx = -1
for j in range(i - 1, -1, -1):
if convo[j]["role"] in ("user", "tool"):
current_idx = j
break
if current_idx < 0:
continue
cur = convo[current_idx]
current = {
"role": cur["role"],
"speaker": cur["role"],
"content": cur["content"],
"channel": "dm",
}
memory = [
{
"role": cm["role"],
"speaker": cm["role"],
"content": cm["content"],
"channel": "dm",
}
for cm in convo[:current_idx]
]
out.append((memory, current, m))
return sys_prompt, out
# ─────────────────────────── per-format adapters ────────────────────────────
_SCAM_DECISION_TO_ELIZA_ACTION = {
# IGNORE-class decisions
"ignore": "IGNORE",
"block": "IGNORE",
"decline": "IGNORE",
"decline_to_answer": "IGNORE",
"refuse": "IGNORE",
# REPLY-class decisions
"reply": "REPLY",
"respond": "REPLY",
"engage": "REPLY",
"accept": "REPLY",
"audit": "REPLY",
"request-verification": "REPLY",
"request_verification": "REPLY",
"verify": "REPLY",
"escalate": "REPLY",
"ask": "REPLY",
"clarify": "REPLY",
}
def _normalize_scam_actions(actions: list) -> list[str]:
"""Map scambench/scam-defense lowercase decision names to canonical
eliza action names (REPLY / IGNORE). Anything unrecognized passes
through uppercased so we don't silently drop unknown actions."""
out: list[str] = []
seen: set[str] = set()
for a in actions or []:
key = str(a).strip().lower().replace("-", "_")
canonical = _SCAM_DECISION_TO_ELIZA_ACTION.get(key, str(a).strip().upper())
if canonical and canonical not in seen:
seen.add(canonical)
out.append(canonical)
if not out:
out = ["REPLY", "IGNORE"]
return out
def scambench_passthrough(records, *, slug, license, split, encoder):
"""ScamBench `eliza` config — emit canonical planner envelope so
`task_type=scam_defense` records share the planner schema with the rest
of the corpus (PIPELINE_SCHEMAS.md §9). The decision class maps to either
REPLY (engage / verify / decline) or IGNORE (block, ignore)."""
for r in records:
meta = r.get("metadata") or {}
decision = (meta.get("decision_class") or "").strip().lower()
reasoning = (meta.get("reasoning_trace") or "").strip()
text = r.get("expectedResponse", "") or ""
if decision in ("ignore", "block", "decline_to_answer", "decline", "refuse"):
target = _planner_ignore_envelope(
thought=reasoning,
text=text,
seed=text,
)
else:
target = _planner_reply_envelope(
thought=reasoning,
text=text, providers=[],
seed=text,
)
expected_response = encoder.encode(target)
yield build(
roomName=r.get("roomName", "") or stable_id(slug, r.get("currentMessage", {}).get("content", "")),
agentId=r.get("agentId", "scam-defense-agent"),
memoryEntries=r.get("memoryEntries") or [],
currentMessage=r.get("currentMessage") or {},
expectedResponse=expected_response,
availableActions=_normalize_scam_actions(
r.get("availableActions") or []
),
task_type="scam_defense",
source_dataset=slug,
license=license,
split=split,
extra_metadata={
"language": meta.get("language", ""),
"scenario_category": meta.get("scenario_category", ""),
"decision_class": meta.get("decision_class", ""),
"should_trigger_scam_defense": meta.get("should_trigger_scam_defense"),
"reasoning_trace": meta.get("reasoning_trace"),
},
)
def hermes_fc(records, *, slug, license, split, encoder):
return _generic_messages(records, slug=slug, license=license, split=split,
messages_key="conversations", encoder=encoder,
default_task_type="tool_call", tools_key="tools")
def hermes_fc_thinking(records, *, slug, license, split, encoder):
return _generic_messages(records, slug=slug, license=license, split=split,
messages_key="conversations", encoder=encoder,
default_task_type="tool_call", tools_key="tools")
def glaive_fc(records, *, slug, license, split, encoder):
"""Glaive function-calling v2 — `chat` is a single string with role markers.
The `-reasoning` shard ships an extra `processed_chat_with_reasoning`
field where each ASSISTANT turn is prefixed with `<think>...</think>`;
we prefer it when present so `_cot_to_expected` can lift the reasoning
into `thought:` instead of dropping it.
"""
for r in records:
if isinstance(r.get("messages"), list):
yield from _generic_messages(iter([r]), slug=slug, license=license, split=split,
messages_key="messages", encoder=encoder,
default_task_type="tool_call", tools_key="tools")
continue
chat = r.get("processed_chat_with_reasoning") or r.get("chat") or ""
sys_prompt = r.get("system") or ""
parts = re.split(r"(USER:|ASSISTANT:|FUNCTION RESPONSE:|SYSTEM:|A:|FUNCTION CALL:|FUNCTION RESULT:)", chat)
msgs: list[dict[str, Any]] = []
i = 1
while i < len(parts) - 1:
marker, content = parts[i], parts[i + 1].strip()
role = {
"USER:": "user", "ASSISTANT:": "assistant", "A:": "assistant",
"FUNCTION RESPONSE:": "tool", "FUNCTION RESULT:": "tool",
"FUNCTION CALL:": "assistant",
"SYSTEM:": "system",
}.get(marker, "user")
msgs.append({"role": role, "content": content})
i += 2
if sys_prompt:
msgs.insert(0, {"role": "system", "content": sys_prompt})
if not msgs:
continue
yield from _generic_messages(iter([{"messages": msgs, "tools": r.get("tools")}]),
slug=slug, license=license, split=split,
messages_key="messages", encoder=encoder,
default_task_type="tool_call", tools_key="tools")
def glaive_fc_reasoning(records, *, slug, license, split, encoder):
return glaive_fc(records, slug=slug, license=license, split=split, encoder=encoder)
def sharegpt_tool_calls(records, *, slug, license, split, encoder):
return _generic_messages(records, slug=slug, license=license, split=split,
messages_key="conversations", encoder=encoder,
default_task_type="tool_call", tools_key="tools")
def functions_53k(records, *, slug, license, split, encoder):
for r in records:
if isinstance(r.get("messages"), list):
yield from _generic_messages(iter([r]), slug=slug, license=license, split=split,
messages_key="messages", encoder=encoder,
default_task_type="tool_call", tools_key="functions")
continue
prompt = r.get("prompt") or r.get("input") or ""
completion = r.get("completion") or r.get("output") or ""
if not prompt or not completion:
continue
calls: list[dict[str, Any]] = []
try:
parsed = json.loads(completion) if isinstance(completion, str) else completion
if isinstance(parsed, dict) and "name" in parsed:
calls = [{"name": parsed["name"], "arguments": parsed.get("arguments") or {}}]
elif isinstance(parsed, list):
calls = [{"name": p.get("name", ""), "arguments": p.get("arguments") or {}}
for p in parsed if isinstance(p, dict)]
except (json.JSONDecodeError, TypeError):
pass
if calls:
target = encoder.encode(_planner_tool_envelope(
thought="", tool_calls=calls, providers=[],
))
actions = [ACTION_TASKS, ACTION_REPLY, ACTION_IGNORE]
tt = "tool_call"
else:
target = _cot_to_expected(encoder, str(completion))
actions = REPLY_ACTIONS.copy()
tt = "reply"
yield build(
roomName=stable_id(slug, r.get("id") or prompt[:120]),
agentId="agent",
currentMessage={"role": "user", "speaker": "user", "content": prompt, "channel": "dm"},
memoryEntries=[],
expectedResponse=target,
availableActions=actions,
task_type=tt,
source_dataset=slug,
license=license,
split=split,
extra_metadata={
"original_id": str(r.get("id") or ""),
"toolSpecs": _normalize_tools(r.get("functions")),
"expected_tool_calls": calls,
},
)
def bitagent(records, *, slug, license, split, encoder):
"""BitAgent/tool_calling — `conversation` and `tools` are stringified JSON.
Roles include 'tool call' and 'tool response' (with content sometimes a dict)."""
def _normalize(r: dict) -> dict:
conv = r.get("conversation") or r.get("conversations") or []
if isinstance(conv, str):
try:
conv = json.loads(conv)
except json.JSONDecodeError:
conv = []
# Map "tool call" / "tool response" roles, and dict-content tool calls.
normalized: list[dict] = []
for m in conv if isinstance(conv, list) else []:
if not isinstance(m, dict):
continue
role = m.get("role", "")
content = m.get("content")
if role == "tool call" and isinstance(content, dict):
normalized.append({
"role": "assistant",
"content": "",
"tool_calls": [{
"type": "function",
"function": {
"name": str(content.get("name") or content.get("tool") or content.get("tool_name") or ""),
"arguments": json.dumps(content.get("arguments") or content.get("args") or {}),
},
}],
})
elif role in ("tool response", "tool"):
normalized.append({"role": "tool", "content": str(content)})
else:
normalized.append({"role": role, "content": str(content) if content is not None else ""})
return {"messages": normalized, "tools": r.get("tools")}
yield from _generic_messages(
(_normalize(r) for r in records),
slug=slug, license=license, split=split,
messages_key="messages", encoder=encoder,
default_task_type="tool_call", tools_key="tools",
)
def toolhop(records, *, slug, license, split, encoder):
"""ToolHop — single Q/A with a list of tool functions. We don't have the
multi-step trace, so we materialize one record per Q with the answer as a
plain reasoning_cot target (the model picks the right tool internally)."""
for r in records:
question = r.get("question") or r.get("query") or ""
answer = str(r.get("answer") or "")
if not question or not answer:
continue
tools_raw = r.get("functions") or r.get("tools") or []
tools_list = _normalize_tools(tools_raw)
yield build(
roomName=stable_id(slug, str(r.get("id") or question[:120])),
agentId="agent",
currentMessage={"role": "user", "speaker": "user", "content": question, "channel": "dm"},
memoryEntries=[],
expectedResponse=_cot_to_expected(encoder, answer),
availableActions=[ACTION_TASKS, ACTION_REPLY, ACTION_IGNORE],
task_type="reasoning_cot",
source_dataset=slug,
license=license,
split=split,
extra_metadata={
"original_id": str(r.get("id") or ""),
"toolSpecs": tools_list,
"domain": str(r.get("domain") or ""),
"answer_type": str(r.get("answer_type") or ""),
},
)
def openclaw_operator(records, *, slug, license, split, encoder):
"""CyberAGI/openclaw-operator-data — actually OpenAI-style messages
`{messages: [...]}` with assistant turns sometimes containing
JSON-encoded tool-call lists. Route through generic messages and let
_extract_tool_calls do its job."""
yield from _generic_messages(
records, slug=slug, license=license, split=split,
messages_key="messages", encoder=encoder,
default_task_type="agent_trace", tools_key="tools",
)
def mobile_actions(records, *, slug, license, split, encoder):
"""google/mobile-actions — `{metadata, tools, messages}`. The assistant
turn embeds the tool call as a JSON list under content; _extract_tool_calls
handles the OpenAI-style tool_calls field too. Treat as tool_call task
but tag task_type=mobile_action via metadata so the manifest separates
mobile from server-side tool calls."""
def _retag(records_iter):
for r in records_iter:
yield {
"messages": r.get("messages") or [],
"tools": r.get("tools") or [],
"_mobile_metadata": r.get("metadata"),
}
yield from _generic_messages(
_retag(records), slug=slug, license=license, split=split,
messages_key="messages", encoder=encoder,
default_task_type="tool_call", tools_key="tools",
)
def nemotron_rl_tool_use(records, *, slug, license, split, encoder):
"""nvidia/Nemotron-RL-Agentic-Conversational-Tool-Use-Pivot-v1 — the
conversation lives under `responses_create_params.input`; tools live
under `responses_create_params.tools`; the supervised target is the
`expected_action` JSON dict."""
def _normalize(r: dict) -> dict:
rcp = r.get("responses_create_params") or {}
msgs = rcp.get("input") or []
tools = rcp.get("tools") or []
# Append the expected_action as the assistant's tool call so
# _extract_tool_calls can lift it out the standard way.
ea = r.get("expected_action") or {}
if isinstance(ea, dict) and ea.get("name"):
msgs = list(msgs) + [{
"role": "assistant",
"content": "",
"tool_calls": [{
"type": "function",
"function": {
"name": ea.get("name", ""),
"arguments": json.dumps(ea.get("arguments") or ea.get("args") or {}),
},
}],
}]
return {"messages": msgs, "tools": tools, "id": r.get("trajectory_id")}
yield from _generic_messages(
(_normalize(r) for r in records),
slug=slug, license=license, split=split,
messages_key="messages", encoder=encoder,
default_task_type="tool_call", tools_key="tools",
)
def qwen36_trajectory(records, *, slug, license, split, encoder):
return _generic_messages(records, slug=slug, license=license, split=split,
messages_key=lambda r: r.get("messages") or r.get("conversations") or r.get("trajectory") or [],
encoder=encoder, default_task_type="agent_trace", tools_key="tools")
def hermes_reasoning_tool_use(records, *, slug, license, split, encoder):
return _generic_messages(records, slug=slug, license=license, split=split,
messages_key=lambda r: r.get("conversations") or r.get("messages") or [],
encoder=encoder, default_task_type="tool_call", tools_key="tools")
def dolci_instruct(records, *, slug, license, split, encoder):
return _generic_messages(records, slug=slug, license=license, split=split,
messages_key=lambda r: r.get("messages") or r.get("conversations") or [],
encoder=encoder, default_task_type="tool_call", tools_key="tools")
def hermes_traces(records, *, slug, license, split, encoder):
return _generic_messages(records, slug=slug, license=license, split=split,
messages_key=lambda r: r.get("conversations") or r.get("messages") or r.get("trajectory") or [],
encoder=encoder, default_task_type="agent_trace", tools_key="tools")
def hermes_omniforge(records, *, slug, license, split, encoder):
return hermes_traces(records, slug=slug, license=license, split=split, encoder=encoder)
def hermes_3(records, *, slug, license, split, encoder):
return _generic_messages(records, slug=slug, license=license, split=split,
messages_key=lambda r: r.get("conversations") or r.get("messages") or [],
encoder=encoder, default_task_type="agent_trace", tools_key="tools")
def aureth(records, *, slug, license, split, encoder):
return hermes_traces(records, slug=slug, license=license, split=split, encoder=encoder)
def nemotron_coding_reasoning(records, *, slug, license, split, encoder):
return hermes_traces(records, slug=slug, license=license, split=split, encoder=encoder)
def hf_coding_tools_traces(records, *, slug, license, split, encoder):
return hermes_traces(records, slug=slug, license=license, split=split, encoder=encoder)
_CHATML_RE = re.compile(
r"<\|im_start\|>\s*(\w+)\s*\n(.*?)<\|im_end\|>",
re.DOTALL,
)
def _parse_chatml(text: str) -> list[dict[str, str]]:
"""Parse Qwen/ChatML <|im_start|>role\\n...<|im_end|> blocks into messages."""
msgs: list[dict[str, str]] = []
for m in _CHATML_RE.finditer(text):
role = (m.group(1) or "").strip().lower()
content = (m.group(2) or "").strip()
if not role:
continue
msgs.append({"role": role, "content": content})
return msgs
def chatml_text(records, *, slug, license, split, encoder):
"""Single `text` field containing a Qwen ChatML conversation."""
for r in records:
text = r.get("text") or ""
if not isinstance(text, str) or "<|im_start|>" not in text:
continue
msgs = _parse_chatml(text)
if not msgs:
continue
yield from _generic_messages(
iter([{"messages": msgs}]),
slug=slug, license=license, split=split,
messages_key="messages", encoder=encoder,
default_task_type="reasoning_cot",
)
_GEMMA_RE = re.compile(
r"<start_of_turn>\s*(\w+)\s*(.*?)<end_of_turn>",
re.DOTALL,
)
# Home-Assistant MCP DSL inside Gemma chat templates:
# <start_function_call>call:NAME{key:<escape>val<escape>,key:val}<end_function_call>
_HA_FUNC_CALL_RE = re.compile(
r"<start_function_call>\s*call:([A-Za-z_][\w]*)\s*\{(.*?)\}\s*<end_function_call>",
re.S,
)
_HA_THINK_RE = re.compile(r"<think>(.*?)</think>", re.S)
_HA_ESCAPE = "<escape>"
def _parse_ha_mcp_args(body: str) -> dict[str, Any]:
"""Parse a HA-MCP DSL argument body into a dict.
Body shape: ``key:<escape>str<escape>,key:42,nested:{...}``. Strings
are wrapped in ``<escape>...<escape>``; bare integers/floats appear
unwrapped.
"""
args: dict[str, Any] = {}
depth = 0
in_escape = False
starts = [0]
i = 0
while i < len(body):
if not in_escape and body.startswith(_HA_ESCAPE, i):
in_escape = True
i += len(_HA_ESCAPE)
continue
if in_escape and body.startswith(_HA_ESCAPE, i):
in_escape = False
i += len(_HA_ESCAPE)
continue
c = body[i]
if not in_escape:
if c in "{[":
depth += 1
elif c in "}]":
depth -= 1
elif c == "," and depth == 0:
starts.append(i + 1)
i += 1
parts: list[str] = []
for j, s in enumerate(starts):
e = starts[j + 1] - 1 if j + 1 < len(starts) else len(body)
parts.append(body[s:e])
for p in parts:
p = p.strip()
if not p:
continue
colon = p.find(":")
if colon < 0:
continue
k = p[:colon].strip()
v = p[colon + 1:].strip()
if v.startswith(_HA_ESCAPE) and v.endswith(_HA_ESCAPE):
args[k] = v[len(_HA_ESCAPE):-len(_HA_ESCAPE)]
else:
try:
args[k] = int(v) if "." not in v else float(v)
except ValueError:
args[k] = v
return args
def _extract_ha_mcp_calls(content: str) -> tuple[list[dict[str, Any]], str, str]:
"""Pull HA-MCP DSL function calls out of an assistant turn.
Returns ``(tool_calls, thought, trailing_text)``. ``thought`` is the
``<think>...</think>`` block (if any). ``trailing_text`` is the
user-facing reply that follows the ``<end_function_response>`` block,
if present.
"""
calls: list[dict[str, Any]] = []
for m in _HA_FUNC_CALL_RE.finditer(content):
calls.append({
"name": m.group(1),
"arguments": _parse_ha_mcp_args(m.group(2)),
})
thought = ""
tm = _HA_THINK_RE.search(content)
if tm:
thought = tm.group(1).strip()
trailing = ""
end_tag = content.rfind("<end_function_response>")
if end_tag >= 0:
trailing = content[end_tag + len("<end_function_response>"):].strip()
return calls, thought, trailing
_HA_FUNC_RESP_RE = re.compile(
r"<start_function_response>(.*?)<end_function_response>",
re.S,
)
def _expand_ha_assistant(content: str) -> list[dict[str, Any]]:
"""Split an HA-MCP assistant turn into ``[assistant_call, tool, assistant_reply]``.
The HA-MCP single-turn assistant string interleaves a ``<think>`` block,
one ``<start_function_call>...<end_function_call>``, one
``<start_function_response>...<end_function_response>``, and a final
user-facing reply. Splitting these into three logical messages lets the
multi-turn record splitter treat the call and the reply as separate
supervised targets.
"""
if "<start_function_call>" not in content:
# Plain assistant reply (HA-MCP also has these — "I'm a smart home
# assistant and can't make phone calls.").
return [{"role": "assistant", "content": content}]
calls, thought, trailing = _extract_ha_mcp_calls(content)
out: list[dict[str, Any]] = []
if calls:
msg: dict[str, Any] = {"role": "assistant", "content": ""}
if thought:
msg["content"] = f"<think>{thought}</think>"
msg["tool_calls"] = [
{
"id": f"call_{i}",
"type": "function",
"function": {
"name": c["name"],
"arguments": json.dumps(c["arguments"]),
},
}
for i, c in enumerate(calls)
]
out.append(msg)
rm = _HA_FUNC_RESP_RE.search(content)
if rm:
out.append({"role": "tool", "content": rm.group(1).strip()})
if trailing:
out.append({"role": "assistant", "content": trailing})
return out
def _parse_gemma(text: str) -> list[dict[str, Any]]:
"""Parse Gemma-style ``<start_of_turn>role ...<end_of_turn>`` into messages.
Assistant turns that embed the HA-MCP ``<start_function_call>`` DSL
are split into ``[assistant_call, tool_response, assistant_reply]`` so
each step is a separate supervised target.
"""
msgs: list[dict[str, Any]] = []
for m in _GEMMA_RE.finditer(text):
role = (m.group(1) or "").strip().lower()
content = (m.group(2) or "").strip()
if not role or not content:
continue
if role in ("model", "assistant"):
msgs.extend(_expand_ha_assistant(content))
else:
msgs.append({"role": role, "content": content})
return msgs
def gemma_text(records, *, slug, license, split, encoder):
"""Single ``text`` field with a Gemma chat-template conversation.
For HA-MCP records, the assistant's DSL function call is hoisted into
OpenAI ``tool_calls`` so the standard tool-call pipeline encodes
it as ``{tool_calls[N]{name,arguments}: ...}``.
"""
for r in records:
text = r.get("text") or ""
if not isinstance(text, str) or "<start_of_turn>" not in text:
continue
msgs = _parse_gemma(text)
if not msgs:
continue
# Use the multi-turn splitter so each assistant turn (call AND
# final reply) becomes a supervised record. For HA-MCP this means
# both the tool call and the trailing user-facing confirmation
# become training rows. Pure-reply records (no DSL call) still
# produce one row per assistant turn.
yield from _mcp_multi_turn(
{"id": r.get("id") or "", "tools": []},
msgs,
slug=slug, license=license, split=split, encoder=encoder,
)
# ─────────────────────── Llama-3 chat template ──────────────────────────────
# Matches one role-block in a Llama-3 chat template:
# <|start_header_id|>ROLE<|end_header_id|>\nCONTENT<|eot_id|> (turn end)
# <|start_header_id|>ROLE<|end_header_id|>\nCONTENT<|eom_id|> (message end, more from same role)
_LLAMA3_RE = re.compile(
r"<\|start_header_id\|>\s*(\w+)\s*<\|end_header_id\|>\s*(.*?)(?=<\|eot_id\|>|<\|eom_id\|>|<\|start_header_id\|>|\Z)",
re.DOTALL,
)
_LLAMA3_PYTHON_TAG_RE = re.compile(r"<\|python_tag\|>(.*?)\Z", re.DOTALL)
_LLAMA3_FUNC_CALL_RE = re.compile(r"^\s*([a-zA-Z_][\w\.\-]*)\s*\((.*)\)\s*$", re.DOTALL)
def _parse_llama3_tool_call(call: str) -> dict[str, Any] | None:
"""Parse a single Llama-3 pythonic tool call: `func_name({json_args})`.
Returns `{"name": ..., "arguments": ...}` or None on unparseable inputs.
Falls back to `{"raw": <args_str>}` arguments when the args region looks
like JSON but contains unescaped quotes (e.g. GraphQL queries embedded
in the string)."""
call = call.strip()
if not call:
return None
m = _LLAMA3_FUNC_CALL_RE.match(call)
if not m:
return None
name = m.group(1)
args_str = m.group(2).strip()
if not args_str:
return {"name": name, "arguments": {}}
try:
args = json.loads(args_str)
if not isinstance(args, dict):
args = {"value": args}
except json.JSONDecodeError:
# Unescaped quotes inside JSON strings (common when the args are
# GraphQL-like queries). Preserve the raw payload so we don't drop
# the row.
args = {"raw": args_str}
return {"name": name, "arguments": args}
def _parse_llama3_chat(text: str) -> list[dict[str, Any]]:
"""Parse Llama-3 `<|start_header_id|>ROLE<|end_header_id|>...` blocks.
Each match becomes one message. Tool calls embedded as `<|python_tag|>`
in an assistant block are surfaced via `tool_calls` so
`_extract_tool_calls` picks them up. The Llama tool role
(`ipython`) maps to canonical `tool` via ROLE_MAP.
"""
msgs: list[dict[str, Any]] = []
for m in _LLAMA3_RE.finditer(text):
role = (m.group(1) or "").strip().lower()
content = (m.group(2) or "").strip()
if not role:
continue
msg: dict[str, Any] = {"role": role, "content": content}
# Pull any tool call out of the assistant content into tool_calls so
# _extract_tool_calls finds it (OpenAI-style path).
if role == "assistant" and "<|python_tag|>" in content:
head, _, tail = content.partition("<|python_tag|>")
tool_calls: list[dict[str, Any]] = []
for raw_call in tail.split("<|python_tag|>"):
parsed = _parse_llama3_tool_call(raw_call)
if parsed:
tool_calls.append({
"type": "function",
"function": {
"name": parsed["name"],
"arguments": json.dumps(parsed["arguments"]),
},
})
if tool_calls:
msg["content"] = head.strip()
msg["tool_calls"] = tool_calls
msgs.append(msg)
return msgs
# ─────────────────── NOESIS plain-text User:/Assistant: ────────────────────
# Match a role marker at line start: `User:` / `Assistant:` / `System:`.
_NOESIS_ROLE_RE = re.compile(r"(?:^|\n)(User|Assistant|System|Human):", re.MULTILINE)
def _parse_noesis_text(text: str) -> list[dict[str, str]]:
"""Split a NOESIS `text` payload into role/content turns.
Format: `User: <q>\\nAssistant: <a>` (optional multi-turn). The blob is
a flat dump with no other delimiters, so we anchor on `\\n(User|Assistant
|System):` line starts and slice between matches.
"""
matches = list(_NOESIS_ROLE_RE.finditer(text))
if not matches:
return []
msgs: list[dict[str, str]] = []
for i, m in enumerate(matches):
role_raw = m.group(1).lower()
role = "user" if role_raw in ("user", "human") else (
"assistant" if role_raw == "assistant" else "system"
)
start = m.end()
end = matches[i + 1].start() if i + 1 < len(matches) else len(text)
content = text[start:end].strip()
if not content:
continue
msgs.append({"role": role, "content": content})
return msgs
# Sentence-end heuristic for filtering NOESIS truncated records. The dataset
# is hard-truncated at `tok_len` tokens; a clean record ends on punctuation,
# a closing bracket/quote/code fence, or a `\boxed{...}` math marker.
_NOESIS_CLEAN_END = re.compile(
r"(?:[.!?。!?\)\]\}»」』]|`{3}|\\boxed\{[^}]+\}|\*\*\.)\s*$"
)
def noesis_text(records, *, slug, license, split, encoder):
"""AMAImedia/NOESIS-1M — `{text, domain, src, tok_len}` rows where `text`
is a flat `User: ...\\nAssistant: ...` dump.
Skips rows that are user-only (the dataset truncates at `tok_len`, so
many reasoning/code rows have no assistant turn) or whose final
assistant turn ends mid-word (truncated supervision target). Multi-turn
rows go through the generic messages path and naturally pick the last
assistant turn as the supervised target. CoT rows (with `<think>`
blocks or in a reasoning/code/math domain) are tagged `reasoning_cot`
so the assistant text passes through as plain text rather than
structured reply.
"""
for r in records:
text = r.get("text") or ""
if not isinstance(text, str) or "User:" not in text:
continue
msgs = _parse_noesis_text(text)
if not msgs:
continue
# Need at least one assistant turn for supervision.
last_asst = None
for m in reversed(msgs):
if m["role"] == "assistant":
last_asst = m
break
if last_asst is None:
continue
# Drop truncated assistant turns: target must end on a sentence
# boundary, closing bracket/quote, `\boxed{...}`, or markdown bold.
asst_text = last_asst["content"].rstrip()
if not asst_text or not _NOESIS_CLEAN_END.search(asst_text):
continue
domain = r.get("domain") or ""
is_reasoning = (
"<think>" in text or domain in ("reasoning", "code", "math", "science", "stem")
)
default_tt = "reasoning_cot" if is_reasoning else "reply"
yield from _generic_messages(
iter([{"messages": msgs}]),
slug=slug, license=license, split=split,
messages_key="messages", encoder=encoder,
default_task_type=default_tt,
)
def open_paws_llama(records, *, slug, license, split, encoder):
"""open-paws/tool-use-llama-format — `messages` is a Llama-3 chat-template
string. Parse role blocks, surface `<|python_tag|>` tool calls as OpenAI
`tool_calls`, then route through the generic messages path so the final
assistant turn becomes either a `tool_call` (structured `tool_calls`) or a
`reply` (structured `thought`/`text`)."""
for r in records:
text = r.get("messages")
if not isinstance(text, str) or "<|start_header_id|>" not in text:
continue
msgs = _parse_llama3_chat(text)
if not msgs:
continue
yield from _generic_messages(
iter([{"messages": msgs}]),
slug=slug, license=license, split=split,
messages_key="messages", encoder=encoder,
default_task_type="reply",
)
# ───────────────────────────── MCP family ───────────────────────────────────
# phi3-mcp DSL: ``TOOL_NEEDED: <name>\nPARAMS: <json>\nREASON: <text>``
_PHI3_TOOL_RE = re.compile(r"TOOL_NEEDED:\s*([^\n]+)", re.S)
_PHI3_PARAMS_RE = re.compile(r"PARAMS:\s*(\{.*?\})\s*(?:\nREASON:|\Z)", re.S)
_PHI3_REASON_RE = re.compile(r"REASON:\s*(.+)\Z", re.S)
def _parse_phi3_output(text: str) -> tuple[dict[str, Any] | None, str]:
"""Parse a phi3-mcp ``output`` string into ``(tool_call_or_None, reason)``."""
if not text:
return None, ""
if "TOOL_NEEDED:" not in text:
return None, text.strip()
name_m = _PHI3_TOOL_RE.search(text)
params_m = _PHI3_PARAMS_RE.search(text)
reason_m = _PHI3_REASON_RE.search(text)
if not name_m:
return None, text.strip()
name = name_m.group(1).strip()
args: dict[str, Any] = {}
if params_m:
try:
parsed = json.loads(params_m.group(1))
if isinstance(parsed, dict):
args = parsed
except json.JSONDecodeError:
args = {"_raw": params_m.group(1)}
reason = reason_m.group(1).strip() if reason_m else ""
return {"name": name, "arguments": args}, reason
def mcp_messages(records, *, slug, license, split, encoder):
"""Generic MCP-style records.
Three shapes are supported:
1. **Multi-turn message lists** (``messages``/``conversations``/...)
are split into one supervised record per assistant turn. Each
assistant turn lands as structured ``tool_calls`` when it carries an
OpenAI-compatible tool call. Otherwise it lands as structured output
``{thought, text}`` for text replies. This recovers tool calls
that live in the middle of agent traces (deepfabric-github-mcp,
playwright-mcp-toolcalling) instead of dropping them in favor of
the final text turn.
2. **Alpaca with phi3-mcp DSL** (``instruction``/``input``/``output``
where ``output`` is ``TOOL_NEEDED: <name>\\nPARAMS: <json>``).
Tool calls land as structured ``tool_calls`` with the reason in
``metadata.tool_reason``; non-tool replies land as structured output
``{thought, text}``.
3. **Generic Alpaca** (``instruction``/``input``/``output``). Plain
text outputs remain replies; tool-call rows must carry structured
fields in the source.
"""
for r in records:
msgs = (
r.get("messages") or r.get("conversations")
or r.get("chat") or r.get("trajectory") or []
)
if msgs:
yield from _mcp_multi_turn(
r, msgs, slug=slug, license=license, split=split,
encoder=encoder,
)
continue
instruction = r.get("instruction") or ""
user_input = r.get("input") or ""
output = r.get("output") or r.get("response") or r.get("completion") or ""
if not output:
continue
prompt_parts = [p for p in (instruction, user_input) if p]
if not prompt_parts:
continue
prompt = "\n\n".join(str(p) for p in prompt_parts)
# phi3-mcp DSL.
call, reason = _parse_phi3_output(str(output))
if call is not None:
target = encoder.encode(_planner_tool_envelope(
thought=reason,
tool_calls=[call], providers=[],
))
actions = [ACTION_TASKS, ACTION_REPLY, ACTION_IGNORE]
tt = "tool_call"
md: dict[str, Any] = {
"original_id": str(r.get("id") or ""),
"expected_tool_calls": [call],
}
if reason:
md["tool_reason"] = reason
yield build(
roomName=stable_id(slug, prompt[:120]),
agentId="mcp-agent",
currentMessage={"role": "user", "speaker": "user", "content": prompt, "channel": "dm"},
memoryEntries=[],
expectedResponse=target,
availableActions=actions,
task_type=tt,
source_dataset=slug,
license=license,
split=split,
extra_metadata=md,
)
continue
# Generic alpaca: probe for embedded tool-call syntaxes.
fake_assistant = {"raw": {}, "content": str(output)}
calls = _extract_tool_calls(fake_assistant)
if calls:
target = encoder.encode(_planner_tool_envelope(
thought="", tool_calls=calls, providers=[],
))
actions = [ACTION_TASKS, ACTION_REPLY, ACTION_IGNORE]
tt = "tool_call"
md = {
"original_id": str(r.get("id") or ""),
"expected_tool_calls": calls,
}
else:
# Plain reply — drop the thought line when there's no upstream
# reasoning to attach (avoids training the model to emit
# `thought: ""`). When the body has <think>...</think> the
# _cot_to_expected helper extracts it automatically.
target = _cot_to_expected(encoder, str(output))
actions = REPLY_ACTIONS.copy()
tt = "reply"
md = {"original_id": str(r.get("id") or "")}
yield build(
roomName=stable_id(slug, prompt[:120]),
agentId="mcp-agent",
currentMessage={"role": "user", "speaker": "user", "content": prompt, "channel": "dm"},
memoryEntries=[],
expectedResponse=target,
availableActions=actions,
task_type=tt,
source_dataset=slug,
license=license,
split=split,
extra_metadata=md,
)
def _mcp_multi_turn(
r: dict[str, Any], msgs_raw: Any, *, slug: str, license: str, split: str,
encoder: ExpectedResponseEncoder,
) -> Iterator[ElizaRecord]:
"""Emit one supervised record per assistant turn in a messages list."""
msgs = _normalize_messages(msgs_raw if isinstance(msgs_raw, list) else [])
if not msgs:
return
sys_prompt, turns = _split_per_turn(msgs)
if not turns:
return
tools_list = _normalize_tools(r.get("tools"))
base_id = str(r.get("id") or "")
for idx, (memory, current, assistant) in enumerate(turns):
extra: dict[str, Any] = {
"original_id": f"{base_id}#{idx}" if base_id else "",
"turn_index": idx,
"turns_total": len(turns),
}
yield _build_messages_record(
slug=slug, license=license, split=split,
sys_prompt=sys_prompt, memory=memory, current=current,
assistant=assistant, encoder=encoder, tools_list=tools_list,
default_task_type="mcp_tool_call",
extra_metadata=extra,
room_seed=f"{base_id}#{idx}" if base_id else f"{current['content'][:120]}#{idx}",
)
def mcp_routing(records, *, slug, license, split, encoder):
for r in records:
if isinstance(r.get("messages"), list) or isinstance(r.get("conversations"), list):
yield from _generic_messages(iter([r]), slug=slug, license=license, split=split,
messages_key=lambda x: x.get("messages") or x.get("conversations") or [],
encoder=encoder, default_task_type="mcp_tool_call", tools_key="tools")
continue
query = r.get("query") or r.get("input") or r.get("instruction") or ""
if not query:
continue
target = {
"server": r.get("server") or r.get("mcp_server") or r.get("expected_server") or "",
"tool": r.get("tool") or r.get("expected_tool") or "",
"arguments": r.get("arguments") or r.get("params") or {},
}
expected_response = encoder.encode(target)
yield build(
roomName=stable_id(slug, r.get("id") or query[:120]),
agentId="mcp-router",
currentMessage={"role": "user", "speaker": "user", "content": query, "channel": "dm"},
memoryEntries=[],
expectedResponse=expected_response,
availableActions=[ACTION_TASKS, ACTION_IGNORE],
task_type="mcp_routing",
source_dataset=slug,
license=license,
split=split,
extra_metadata={"original_id": str(r.get("id") or "")},
)
def _mcp_flow_parse_function_call(fc: Any) -> dict[str, Any] | None:
"""Decode the `function_call` field, which may be a `{name, arguments}`
dict or a JSON-encoded string of the same."""
if isinstance(fc, str):
try:
fc = json.loads(fc)
except json.JSONDecodeError:
return None
if not isinstance(fc, dict):
return None
name = fc.get("name") or ""
args = fc.get("arguments")
if isinstance(args, str):
try:
args = json.loads(args)
except json.JSONDecodeError:
args = {"raw": args}
if not name:
return None
return {"name": name, "arguments": args if isinstance(args, dict) else {}}
def mcp_flow(records, *, slug, license, split, encoder):
"""wwh0411/MCP-Flow — two record shapes both surface as `mcp_tool_call`:
1. `function_call/<provider>/<server>.json` — list of
`{source_instruction, function_call: {name, arguments}, tool}` rows.
One supervised tool call per row, anchored on `source_instruction`.
2. `test_data/*.json` — list of
`{instruction, server_name, tool_name, function_call: <json-str>,
tools: <json-str>, conversations}` rows. The `tools` field carries
the full set of tool specs available; we keep it under
`metadata.toolSpecs`.
The legacy `{name, examples}` per-tool-spec shape this adapter formerly
targeted is not present in the dataset as shipped — there are no
`examples` arrays anywhere under `function_call/`. So we only handle
the two real shapes above.
"""
for r in records:
# Shape 2: test_data with full tool list.
if "instruction" in r and "function_call" in r:
user_q = r.get("instruction") or ""
call = _mcp_flow_parse_function_call(r.get("function_call"))
if not user_q or not call:
continue
tools_raw = r.get("tools")
tools_list = _normalize_tools(tools_raw)
calls = [call]
expected_response = encoder.encode(_planner_tool_envelope(
thought="", tool_calls=calls, providers=[],
))
yield build(
roomName=stable_id(slug, call["name"], user_q[:80]),
agentId="mcp-agent",
currentMessage={"role": "user", "speaker": "user", "content": user_q, "channel": "dm"},
memoryEntries=[],
expectedResponse=expected_response,
availableActions=[ACTION_TASKS, ACTION_IGNORE],
task_type="mcp_tool_call",
source_dataset=slug,
license=license,
split=split,
extra_metadata={
"server_name": r.get("server_name") or "",
"tool_name": call["name"],
"toolSpecs": tools_list,
"expected_tool_calls": calls,
},
)
continue
# Shape 1: function_call/<provider>/<server>.json single example.
if "source_instruction" in r and "function_call" in r:
user_q = r.get("source_instruction") or ""
call = _mcp_flow_parse_function_call(r.get("function_call"))
if not user_q or not call:
continue
calls = [call]
expected_response = encoder.encode(_planner_tool_envelope(
thought="", tool_calls=calls, providers=[],
))
yield build(
roomName=stable_id(slug, call["name"], user_q[:80]),
agentId="mcp-agent",
currentMessage={"role": "user", "speaker": "user", "content": user_q, "channel": "dm"},
memoryEntries=[],
expectedResponse=expected_response,
availableActions=[ACTION_TASKS, ACTION_IGNORE],
task_type="mcp_tool_call",
source_dataset=slug,
license=license,
split=split,
extra_metadata={
"tool_name": call["name"],
"expected_tool_calls": calls,
},
)
continue
# ─────────────────── shell / terminal / agent_trove ─────────────────────────
def _shell_target(command: str, explanation: str = "", cwd: str = "") -> dict[str, Any]:
"""Build a SHELL planner-envelope target.
Returns the canonical 5-key planner envelope (PIPELINE_SCHEMAS.md §1+§7)
with `actions[].name == SHELL` carrying the shell parameters.
The `explanation` is folded into `thought:` when present; otherwise we
use the generic shell default.
"""
return _planner_shell_envelope(
thought=_strip_surrogates(explanation),
command=command, explanation=explanation, cwd=cwd,
text="", providers=[],
)
def _terminal_assistant_extract(content: str) -> tuple[str, str]:
"""Parse a nemotron-terminal-corpus / agent-trove style assistant turn.
The conversational shape is `<think>...</think>\\n\\n{"analysis":...,
"plan":..., "commands":[{"keystrokes":...}, ...], "task_complete":...}`.
Some turns ship the JSON without the `<think>` prefix; some ship a
fenced bash block instead of JSON.
Returns (command, explanation) where:
- command: keystrokes joined with `\\n`, or the fenced bash text,
or the raw content fallback.
- explanation: any extracted thought (`<think>` body) plus the
`analysis` / `plan` text from the JSON envelope.
Empty string if nothing usable is found.
"""
content = (content or "").strip()
if not content:
return "", ""
explanation_parts: list[str] = []
# 1. <think>…</think> prefix carries the planner reasoning.
mt = _THINK_RE.match(content)
if mt:
thought = mt.group(1).strip()
if thought:
explanation_parts.append(thought)
body = content[mt.end():].strip()
else:
body = content
# 2. JSON envelope: {"analysis": ..., "plan": ..., "commands": [...]}.
cmd = ""
is_json_envelope = False
if body.startswith("{") and body.endswith("}"):
try:
obj = json.loads(body)
except (json.JSONDecodeError, ValueError):
obj = None
if isinstance(obj, dict) and ("commands" in obj or "analysis" in obj or "plan" in obj):
is_json_envelope = True
analysis = obj.get("analysis")
plan = obj.get("plan")
if isinstance(analysis, str) and analysis.strip():
explanation_parts.append(analysis.strip())
if isinstance(plan, str) and plan.strip():
explanation_parts.append("Plan: " + plan.strip())
commands = obj.get("commands")
if isinstance(commands, list):
ks_parts: list[str] = []
for c in commands:
if isinstance(c, dict):
ks = c.get("keystrokes")
if isinstance(ks, str) and ks.strip():
ks_parts.append(ks.rstrip("\n"))
if ks_parts:
cmd = "\n".join(ks_parts)
# 3. Fenced bash block fallback (when there was no JSON envelope).
if not cmd and not is_json_envelope:
for m in re.finditer(r"```(?:bash|sh)?\s*\n([\s\S]*?)```", body):
cmd = m.group(1).strip()
break
if not cmd and not is_json_envelope:
cmd = body
# If we recognized a JSON envelope but the commands list was empty,
# this is a `task_complete: true` terminator with no shell command —
# not a real shell_command record. Caller should drop it.
if is_json_envelope and not cmd:
return "", ""
explanation = "\n\n".join(p for p in explanation_parts if p).strip()
return cmd, explanation
def terminal_corpus(records, *, slug, license, split, encoder):
"""laion/nemotron-terminal-corpus-unified — emit SHELL records."""
for r in records:
# The corpus has a few shapes; we try common ones.
if isinstance(r.get("messages"), list) or isinstance(r.get("conversations"), list):
msgs = r.get("messages") or r.get("conversations") or []
sys_prompt, memory, current, final = _split_history(msgs)
if not final or not current:
continue
command, explanation = _terminal_assistant_extract(final.get("content", "") or "")
if not command:
continue
expected_response = encoder.encode(_shell_target(command, explanation))
yield build(
roomName=stable_id(slug, current["content"][:120]),
agentId="agent",
memoryEntries=memory,
currentMessage=current,
expectedResponse=expected_response,
availableActions=[ACTION_SHELL, ACTION_REPLY, ACTION_IGNORE],
task_type="shell_command",
source_dataset=slug,
license=license,
split=split,
extra_metadata={"system_prompt": sys_prompt} if sys_prompt else {},
)
continue
instruction = r.get("instruction") or r.get("query") or r.get("prompt") or ""
command = r.get("command") or r.get("output") or r.get("response") or ""
explanation = r.get("explanation") or r.get("rationale") or r.get("reasoning") or ""
if not instruction or not command:
continue
expected_response = encoder.encode(_shell_target(str(command), str(explanation)))
yield build(
roomName=stable_id(slug, r.get("id") or instruction[:120]),
agentId="agent",
memoryEntries=[],
currentMessage={"role": "user", "speaker": "user", "content": instruction, "channel": "dm"},
expectedResponse=expected_response,
availableActions=[ACTION_SHELL, ACTION_REPLY, ACTION_IGNORE],
task_type="shell_command",
source_dataset=slug,
license=license,
split=split,
extra_metadata={"original_id": str(r.get("id") or "")},
)
def agent_trove(records, *, slug, license, split, encoder):
"""open-thoughts/AgentTrove — agent trajectories. Use generic messages
path; if the final assistant turn looks like a shell command, label as
shell_command, else tool_call/agent_trace.
For shell-command turns we lift `analysis` + `plan` out of the JSON
envelope into `explanation:`. For agent_trace turns the same fields
are lifted into `thought:` by `_build_messages_record` via
`_split_thought_and_body` / `_extract_agent_trove_json_thought`.
"""
for r in records:
msgs = r.get("messages") or r.get("conversations") or r.get("trajectory") or []
if not msgs:
continue
sys_prompt, memory, current, final = _split_history(msgs)
if not final or not current:
continue
content = final.get("content", "") or ""
# Check for agent-trove JSON envelope (preferred) or fenced bash.
is_json_shell = False
body = content.strip()
if body.startswith("{") and body.endswith("}"):
try:
obj = json.loads(body)
if isinstance(obj, dict) and isinstance(obj.get("commands"), list) \
and any(isinstance(c, dict) and c.get("keystrokes") for c in obj.get("commands") or []):
is_json_shell = True
except (json.JSONDecodeError, ValueError):
pass
# Also handle <think>...</think>{...json envelope...}.
if not is_json_shell:
mt = _THINK_RE.match(body)
if mt:
rest = body[mt.end():].strip()
if rest.startswith("{") and rest.endswith("}"):
try:
obj = json.loads(rest)
if isinstance(obj, dict) and isinstance(obj.get("commands"), list) \
and any(isinstance(c, dict) and c.get("keystrokes") for c in obj.get("commands") or []):
is_json_shell = True
except (json.JSONDecodeError, ValueError):
pass
m = re.search(r"```(?:bash|sh)\s*\n([\s\S]*?)```", content) if not is_json_shell else None
if is_json_shell or m:
command, explanation = _terminal_assistant_extract(content)
if not command:
# task_complete: true terminator with no actual shell
# command — drop it (audit B-4 confirms these are noise).
continue
expected_response = encoder.encode(_shell_target(command, explanation))
yield build(
roomName=stable_id(slug, r.get("id") or current["content"][:120]),
agentId="agent",
memoryEntries=memory,
currentMessage=current,
expectedResponse=expected_response,
availableActions=[ACTION_SHELL, ACTION_REPLY, ACTION_IGNORE],
task_type="shell_command",
source_dataset=slug,
license=license,
split=split,
extra_metadata={"system_prompt": sys_prompt} if sys_prompt else {},
)
continue
# Fall through to generic: agent_trace / tool_call / reply, with
# `_build_messages_record` lifting analysis/plan into `thought:`.
tools_list = _normalize_tools(r.get("tools"))
yield _build_messages_record(
slug=slug, license=license, split=split,
sys_prompt=sys_prompt, memory=memory, current=current,
assistant=final, encoder=encoder, tools_list=tools_list,
default_task_type="agent_trace",
extra_metadata={"original_id": str(r.get("id") or "")},
)
# ───────────────────────── reasoning / CoT family ───────────────────────────
def reasoning_cot(records, *, slug, license, split, encoder):
"""Generic reasoning/CoT corpora (Jackrong DeepSeek/GLM/Kimi/Qwen/glm-4.7,
open-paws, Akicou, and friends).
The supervised target is structured `{thought, text}`. Source corpora ship
a `<think>…</think>` block followed by the answer; we extract the
block into `thought` and put the remainder in `text`. Tool calls
inside take the tool_call path.
"""
for r in records:
msgs = (
r.get("messages") or r.get("conversations") or r.get("conversation")
or r.get("trajectory") or r.get("dialogue") or []
)
if not msgs:
# Some Jackrong shards ship {"prompt", "response"} pairs.
prompt = r.get("prompt") or r.get("instruction") or r.get("input") or ""
response = r.get("response") or r.get("output") or r.get("completion") or ""
if not prompt or not response:
continue
yield build(
roomName=stable_id(slug, r.get("id") or prompt[:120]),
agentId="agent",
currentMessage={"role": "user", "speaker": "user", "content": str(prompt), "channel": "dm"},
memoryEntries=[],
expectedResponse=_cot_to_expected(encoder, str(response)),
availableActions=REPLY_ACTIONS.copy(),
task_type="reasoning_cot",
source_dataset=slug,
license=license,
split=split,
extra_metadata={"original_id": str(r.get("id") or "")},
)
continue
sys_prompt, memory, current, final = _split_history(msgs)
if not final or not current:
continue
calls = _extract_tool_calls(final)
text = final.get("content", "") or ""
extra_thought = str(final.get("_pending_thought") or "")
thought, body = _split_thought_and_body(text)
if extra_thought:
thought = (extra_thought.strip() + ("\n\n" + thought if thought else "")).strip()
if calls:
target = encoder.encode(_planner_tool_envelope(
thought=thought, tool_calls=calls, text=body, providers=[],
))
actions = [ACTION_TASKS, ACTION_REPLY, ACTION_IGNORE]
tt = "tool_call"
else:
target = _cot_to_expected(encoder, text, extra_thought=extra_thought)
actions = REPLY_ACTIONS.copy()
tt = "reasoning_cot"
md = {"original_id": str(r.get("id") or "")}
if sys_prompt:
md["system_prompt"] = sys_prompt
if calls:
md["expected_tool_calls"] = calls
yield build(
roomName=stable_id(slug, r.get("id") or current["content"][:120]),
agentId="agent",
memoryEntries=memory,
currentMessage=current,
expectedResponse=target,
availableActions=actions,
task_type=tt,
source_dataset=slug,
license=license,
split=split,
extra_metadata=md,
)
# ─────────────── nubilio trajectories (self-hosted eliza bot) ──────────────
# Filename → (task_type, available_actions)
#
# The first five files are emitted by elizaOS app-training's
# `exportTrajectoryTaskDatasets` (eliza/apps/app-training/src/core/
# trajectory-task-datasets.ts). Three of them — should_respond,
# context_routing, media_description — ship as empty files until the
# nubilio runtime actually emits LLM calls with the matching purpose
# hints (see `inferTasksForCall` in that file). They become populated
# automatically once those code paths fire (LLM-based shouldRespond
# evaluation, context-routing decisions, image-description model
# calls); no adapter change is needed when that happens.
#
# `reflection_trajectories.jsonl` and `reflection_evaluator_trajectories.jsonl`
# are forward-compat entries for elizaOS reflection prompts. They are mapped
# here so the runtime can start writing them without a follow-up adapter patch.
_NUBILIO_TASK_MAP: dict[str, tuple[str, list[str]]] = {
"action_planner_trajectories.jsonl": ("agent_trace", [ACTION_REPLY, ACTION_TASKS, ACTION_IGNORE]),
"response_trajectories.jsonl": ("reply", REPLY_ACTIONS.copy()),
"should_respond_trajectories.jsonl": ("should_respond", ROUTING_ACTIONS.copy()),
"context_routing_trajectories.jsonl": ("context_routing", ROUTING_ACTIONS.copy()),
"media_description_trajectories.jsonl": ("media_description", REPLY_ACTIONS.copy()),
"reflection_trajectories.jsonl": ("reflection", REPLY_ACTIONS.copy()),
"reflection_evaluator_trajectories.jsonl": ("reflection_evaluator", REPLY_ACTIONS.copy()),
}
def _coerce_scalar(s: str) -> Any:
"""Best-effort cast of a string to bool/int/float/null, else strip & return."""
t = s.strip()
if t == "":
return ""
if t.lower() == "true":
return True
if t.lower() == "false":
return False
if t.lower() in ("null", "none"):
return None
if re.fullmatch(r"-?\d+", t):
return int(t)
if re.fullmatch(r"-?\d+\.\d+", t):
return float(t)
return t
def _xml_element_to_value(el: Any) -> Any:
"""Convert an ElementTree element to a JSON-friendly value.
Leaf elements → coerced scalar. Elements with children → dict mapping
child tag → value. Repeated child tags collapse into a list.
"""
children = list(el)
text = (el.text or "").strip()
if not children:
return _coerce_scalar(text)
out: dict[str, Any] = {}
for child in children:
val = _xml_element_to_value(child)
if child.tag in out:
existing = out[child.tag]
if isinstance(existing, list):
existing.append(val)
else:
out[child.tag] = [existing, val]
else:
out[child.tag] = val
# Preserve text content alongside children when both exist (rare).
if text:
out.setdefault("_text", _coerce_scalar(text))
return out
def _parse_response_xml(xml: str) -> dict[str, Any] | None:
"""Parse the elizaOS planner `<response>...</response>` blob into a dict.
Tolerates the common `</actions>` typo where `<action>` close tags are
missing. Falls back to None if parsing fails entirely so the caller
can hold the original string instead of corrupting the corpus.
"""
body = xml.strip()
if not body.startswith("<response>"):
return None
# Tolerate the LLM's common malformed pattern:
# <action><name>X</name></actions>
# where the trailing close tag should have been </action></actions>.
# We only patch when we see an unmatched </actions>.
import xml.etree.ElementTree as ET # stdlib
try:
root = ET.fromstring(body)
except ET.ParseError:
# Pattern A: `<action>...</actions>` with no `</action>` close.
patched = re.sub(
r"(<action>\s*<name>[^<]*</name>)\s*</actions>",
r"\1</action></actions>",
body,
)
patched = re.sub(
r"(<action>\s*<name>[^<]*</name>\s*<params>[\s\S]*?</params>)\s*</actions>",
r"\1</action></actions>",
patched,
)
# Pattern B: doubled `</actions></actions>` after the patch (or
# in the original). Collapse to one.
patched = re.sub(r"(</actions>)(\s*</actions>)+", r"\1", patched)
try:
root = ET.fromstring(patched)
except ET.ParseError:
return None
if root.tag != "response":
return None
out: dict[str, Any] = {}
for child in root:
tag = child.tag
if tag == "actions":
actions: list[Any] = []
for action_el in child.findall("action"):
a = _xml_element_to_value(action_el)
# Common case: <action><name>NAME</name></action> → string "NAME"
if isinstance(a, dict) and set(a.keys()) == {"name"}:
actions.append(a["name"])
else:
actions.append(a if isinstance(a, dict) else {"name": a})
out["actions"] = actions
elif tag == "providers":
providers: list[Any] = []
for p_el in child.findall("provider"):
p = _xml_element_to_value(p_el)
providers.append(p["name"] if isinstance(p, dict) and set(p.keys()) == {"name"} else p)
# Empty <providers></providers> → []
out["providers"] = providers
else:
out[tag] = _xml_element_to_value(child)
return out
_YAML_KEY_LINE = re.compile(r"^([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*(.*)$")
def _parse_yaml_thought(text: str) -> dict[str, Any] | None:
"""Parse a `key: value\\nkey2: value2` block into a dict.
Used for the planner's "yaml-style" outputs (mostly evaluation-purpose
LLM calls that emit `thought: …\\ntext: …`). Tolerates multi-line
string values via continuation indentation.
"""
body = text.strip()
if not body:
return None
out: dict[str, Any] = {}
current_key: str | None = None
buf: list[str] = []
def flush() -> None:
if current_key is None:
return
joined = "\n".join(buf).strip()
# Strip surrounding quotes if present.
if (joined.startswith('"') and joined.endswith('"')) or \
(joined.startswith("'") and joined.endswith("'")):
joined = joined[1:-1]
out[current_key] = _coerce_scalar(joined) if "\n" not in joined else joined
for raw in body.splitlines():
m = _YAML_KEY_LINE.match(raw)
if m and (raw[0:1].isalpha() or raw[0:1] == "_"):
flush()
current_key = m.group(1)
buf = [m.group(2)]
else:
if current_key is None:
return None
buf.append(raw)
flush()
if not out:
return None
return out
_MD_JSON_FENCE = re.compile(r"^```(?:json)?\s*\n([\s\S]*?)\n```\s*$", re.MULTILINE)
def _parse_md_json_fence(text: str) -> Any | None:
body = text.strip()
m = _MD_JSON_FENCE.match(body)
if not m:
return None
try:
return json.loads(m.group(1))
except json.JSONDecodeError:
return None
def _nubilio_response_to_dict(text: str) -> tuple[dict[str, Any] | list[Any] | None, str]:
"""Best-effort parse of a nubilio assistant turn into a structured value.
Returns (parsed_value, source_format). When parsing fails, returns
(None, "raw"). Recognized formats:
- "xml-response" : full <response>...</response> planner XML
- "json-obj" : a top-level JSON object (e.g. {"providers":[]})
- "json-array" : a JSON array
- "yaml-thought" : `key: value` block (often `thought:` / `text:`)
- "md-fence" : ```json …``` fenced JSON
- "raw" : unparseable; fall back to {thought:"", text:<raw>}
"""
body = text.strip()
if body.startswith("<response>"):
parsed = _parse_response_xml(body)
if parsed is not None:
return parsed, "xml-response"
if body.startswith("{"):
try:
return json.loads(body), "json-obj"
except json.JSONDecodeError:
pass
if body.startswith("["):
try:
return json.loads(body), "json-array"
except json.JSONDecodeError:
pass
if body.startswith("```"):
parsed = _parse_md_json_fence(body)
if parsed is not None:
return parsed, "md-fence"
if re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*\s*:", body):
parsed = _parse_yaml_thought(body)
if parsed is not None:
return parsed, "yaml-thought"
return None, "raw"
def nubilio_trajectories(records, *, slug, license, split, encoder):
"""Cron-snapshot trajectories from the self-hosted nubilio eliza bot.
Each line is `{"messages": [system, user, ..., assistant]}`. The
assistant content is parsed (XML / JSON / YAML-thought) and re-encoded
with the configured expected-response encoder so the supervised target matches the elizaOS runtime decoder.
Filename selects task_type via `_NUBILIO_TASK_MAP`. Cross-file dedup
uses the (system, last-user, assistant) triple.
"""
seen: set[str] = set()
for r in records:
msgs = r.get("messages") or []
if not msgs:
continue
sys_prompt, memory, current, final = _split_history(msgs)
if not final or not current:
continue
assistant_text = final.get("content") or ""
if not assistant_text.strip():
continue
source_file = r.get("_source_filename", "")
task_type, actions = _NUBILIO_TASK_MAP.get(
source_file, ("agent_trace", [ACTION_REPLY, ACTION_TASKS, ACTION_IGNORE]),
)
dedup = stable_id(sys_prompt[:512], current["content"][:512], assistant_text[:512])
if dedup in seen:
continue
seen.add(dedup)
parsed, fmt = _nubilio_response_to_dict(assistant_text)
if parsed is None:
# Plain text reply → emit as structured `{text}` (drop empty thought
# so the student model doesn't learn to produce `thought: ""`).
# Any embedded `<think>` block is lifted by `_cot_to_expected`.
target = _cot_to_expected(encoder, assistant_text)
else:
try:
target = encoder.encode(parsed)
except (ValueError, TypeError):
# Fall back to wrapping the raw assistant text — keeps the
# supervised target valid structured output even when the structured parse
# produced something the encoder rejects.
target = _cot_to_expected(encoder, assistant_text)
fmt = "raw"
md: dict[str, Any] = {
"original_id": dedup,
"nubilio_source_file": source_file,
"nubilio_response_format": fmt,
}
if sys_prompt:
md["system_prompt"] = sys_prompt
yield build(
roomName=stable_id(slug, source_file, dedup),
agentId="remilio-nubilio",
memoryEntries=memory,
currentMessage=current,
expectedResponse=target,
availableActions=actions,
task_type=task_type,
source_dataset=slug,
license=license,
split=split,
extra_metadata=md,
)
# ───────────── eliza_native_v1 nightly-export passthrough ──────────────────
def _eliza_native_extract_messages(messages: list[Any]) -> tuple[
str, list[dict[str, Any]], dict[str, Any] | None,
]:
"""Return (system_prompt, memory_entries, current_message) for ElizaRecord.
Splits the trajectory message list the same way `_split_history` does:
leading system turns become the system prompt, the last user turn is the
current message, the remainder is the memory window. Tool turns ride
along in memory.
"""
sys_parts: list[str] = []
memory: list[dict[str, Any]] = []
last_user: dict[str, Any] | None = None
for raw in messages:
if not isinstance(raw, dict):
continue
role = raw.get("role")
content = raw.get("content")
if role == "system":
if isinstance(content, str):
sys_parts.append(content)
continue
if role == "user":
if last_user is not None:
memory.append(last_user)
last_user = {"role": "user", "content": content}
continue
if role in {"assistant", "tool"}:
if last_user is not None:
memory.append(last_user)
last_user = None
memory.append({"role": role, "content": content})
return ("\n".join(sys_parts).strip(), memory, last_user)
def eliza_native_passthrough(records, *, slug, license, split, encoder):
"""Passthrough adapter for already-`eliza_native_v1` JSONL rows.
Used by the nightly trajectory-export bridge: the TS pipeline writes
sanitized `eliza_native_v1` rows to disk, this adapter reads them and
re-emits them as `ElizaRecord` so the existing pack/format pipeline
picks them up unchanged. No additional privacy filtering — the TS
export already applied the runtime privacy filter on its write path.
Rows that fail `validate_native_record` are dropped with an
`errors.jsonl` entry, same as every other adapter.
"""
from .native_record import FORMAT as NATIVE_FORMAT, validate_native_record
for raw in records:
if not isinstance(raw, dict):
continue
ok, why = validate_native_record(raw)
if not ok:
# Mark invalid by emitting an ElizaRecord that will fail
# is_valid(); the caller writes it to errors.jsonl.
yield build(
roomName=stable_id(slug, "invalid", raw.get("trajectoryId", "")),
agentId="unknown",
expectedResponse="",
task_type=f"invalid:{why}",
source_dataset=slug,
license=license,
split=split,
)
continue
request = raw.get("request") or {}
response = raw.get("response") or {}
metadata_in = raw.get("metadata") or {}
sys_prompt, memory, current = _eliza_native_extract_messages(
request.get("messages") or []
)
if current is None:
prompt_text = request.get("prompt")
if isinstance(prompt_text, str) and prompt_text.strip():
current = {"role": "user", "content": prompt_text}
if current is None:
continue
expected = response.get("text")
if not isinstance(expected, str) or not expected.strip():
continue
task_type = (
metadata_in.get("task_type")
or metadata_in.get("task")
or "agent_trace"
)
agent_id = str(metadata_in.get("agent_id") or raw.get("agentId") or "unknown")
trajectory_id = str(
metadata_in.get("trajectory_id") or raw.get("trajectoryId") or ""
)
extra_md: dict[str, Any] = {
"eliza_native_format": NATIVE_FORMAT,
"boundary": raw.get("boundary", ""),
}
if sys_prompt:
extra_md["system_prompt"] = sys_prompt
if trajectory_id:
extra_md["trajectory_id"] = trajectory_id
call_id = metadata_in.get("call_id") or raw.get("callId")
if call_id:
extra_md["call_id"] = str(call_id)
yield build(
roomName=stable_id(slug, trajectory_id or "row", str(call_id or "")),
agentId=agent_id,
memoryEntries=memory,
currentMessage=current,
expectedResponse=expected,
availableActions=[],
task_type=str(task_type),
source_dataset=slug,
license=license,
split=split,
extra_metadata=extra_md,
)
# ───────────── scam-defense corpus (full-corpus-unweighted) ────────────────
# Categories that are NOT scam-defense scenarios. Anything not in this set
# is treated as a scam-defense interaction.
_LEGITIMATE_CATEGORIES = {
"legitimate", "benign", "banking-inquiry", "security-inquiry",
"small-talk", "general",
}
def _scam_defense_flag(category: str | None) -> bool:
if not category:
return False
if category.startswith("legitimate"):
return False
return category not in _LEGITIMATE_CATEGORIES
def _normalize_action(action: str) -> str:
"""request-verification → request_verification (match scambench shape)."""
return action.replace("-", "_").strip().lower() if action else ""
def _parse_scam_user_prompt(prompt: str) -> tuple[dict[str, Any] | None, list[dict[str, str]]]:
"""Split the scam-defense userPrompt into (runtime_context, transcript).
The userPrompt has the shape:
Runtime context:
{ ...JSON... }
Conversation transcript:
[Speaker]: line
[Speaker]: line
...
"""
ctx: dict[str, Any] | None = None
transcript: list[dict[str, str]] = []
parts = prompt.split("Conversation transcript:", 1)
if len(parts) == 2:
head, tail = parts
# Find the runtime-context JSON object
ctx_start = head.find("{")
if ctx_start != -1:
depth = 0
for i, ch in enumerate(head[ctx_start:], start=ctx_start):
if ch == "{":
depth += 1
elif ch == "}":
depth -= 1
if depth == 0:
try:
ctx = json.loads(head[ctx_start:i + 1])
except json.JSONDecodeError:
ctx = None
break
body = tail
else:
body = prompt
for raw_line in body.splitlines():
line = raw_line.strip()
if not line.startswith("["):
continue
end = line.find("]:")
if end == -1:
continue
speaker = line[1:end].strip()
content = line[end + 2:].strip()
if not speaker or not content:
continue
transcript.append({"speaker": speaker, "content": content})
return ctx, transcript
def scam_defense_corpus(records, *, slug, license, split, encoder):
"""Full-corpus-unweighted scam-defense trajectories.
Each record is `{"trajectory": {steps: [{llmCalls: [...]}]}}`. Each
llmCall has systemPrompt, userPrompt (runtime_context + transcript),
response (often `<think>…</think>\\n<final text>`). Emits one
ElizaRecord per llmCall with task_type=`scam_defense` and
expectedResponse passed through verbatim (preserves the `<think>`
block alongside the final reply).
"""
seen: set[str] = set()
for r in records:
traj = r.get("trajectory") or {}
agent_id = str(traj.get("agentId") or "agent")
traj_id = str(traj.get("id") or traj.get("trajectoryId") or "")
archetype = traj.get("archetype") or ""
meta_json: dict[str, Any] = {}
raw_meta = traj.get("metadataJson")
if isinstance(raw_meta, str):
try:
meta_json = json.loads(raw_meta)
except json.JSONDecodeError:
meta_json = {}
elif isinstance(raw_meta, dict):
meta_json = raw_meta
for step in traj.get("steps") or []:
for call_idx, call in enumerate(step.get("llmCalls") or []):
sys_prompt = str(call.get("systemPrompt") or "")
user_prompt = str(call.get("userPrompt") or "")
response = str(call.get("response") or "")
if not user_prompt or not response:
continue
ctx, transcript = _parse_scam_user_prompt(user_prompt)
if not transcript:
continue
memory = [
{
"role": "assistant" if t["speaker"] == agent_id else "user",
"speaker": t["speaker"],
"content": t["content"],
"channel": "dm",
}
for t in transcript[:-1]
]
last = transcript[-1]
# Drop trailing agent turns; we want the most recent inbound turn
# as currentMessage so the supervised target is the next reply.
while memory and last["speaker"] == agent_id:
last = memory.pop()
if last["speaker"] == agent_id:
continue
current = {
"role": "user",
"speaker": last["speaker"],
"content": last["content"],
"channel": "dm",
}
dedup = stable_id(traj_id, step.get("stepNumber", 0), call_idx, response[:512])
if dedup in seen:
continue
seen.add(dedup)
action = step.get("action") or {}
action_type = action.get("actionType") or call.get("actionType") or ""
params = action.get("parameters") or {}
chosen_action = (
params.get("chosenAction")
or meta_json.get("chosenAction")
or ""
)
avail = []
if isinstance(ctx, dict):
for a in ctx.get("availableActions") or []:
if isinstance(a, dict) and a.get("name"):
avail.append(a["name"])
if not avail:
avail = REPLY_ACTIONS.copy()
# Normalize lowercase scam-defense decision names
# (refuse / escalate / accept / etc.) to canonical eliza
# actions (REPLY / IGNORE) — eliza runtime parsers expect
# uppercase.
avail = _normalize_scam_actions(avail)
category = meta_json.get("category")
reasoning, final_text = _split_think_response(response)
# Prefer the action.result.responseText when present — that's
# the canonical agent reply; the LLM `response` field may
# carry trailing chain-of-thought we already split out.
result_text = ""
if isinstance(action.get("result"), dict):
result_text = str(action["result"].get("responseText") or "")
final_response = result_text or final_text
# Map the upstream decision class to a planner-envelope
# action (PIPELINE_SCHEMAS.md §9). `block` / `ignore` /
# `decline` / `refuse` → IGNORE; everything else → REPLY.
norm_action = _normalize_action(chosen_action).lower()
if norm_action in ("ignore", "block", "decline_to_answer", "decline", "refuse"):
target = _planner_ignore_envelope(
thought=reasoning,
text=final_response,
seed=final_response,
)
else:
target = _planner_reply_envelope(
thought=reasoning,
text=final_response, providers=[],
seed=final_response,
)
target = encoder.encode(target)
md: dict[str, Any] = {
"original_id": dedup,
"trajectory_id": traj_id,
"step_number": step.get("stepNumber"),
"call_index": call_idx,
"archetype": archetype,
"purpose": call.get("purpose"),
"action_type": action_type,
"chosen_action": chosen_action,
"category": category,
"scenario_category": category,
"should_trigger_scam_defense": _scam_defense_flag(category),
"language": meta_json.get("language"),
"style_variant": meta_json.get("styleVariant"),
"scenario_profile": meta_json.get("scenarioProfile"),
"source_pool": meta_json.get("sourcePool"),
"has_reasoning": meta_json.get("hasReasoning"),
"reasoning_source": call.get("reasoningSource"),
"reward": step.get("reward"),
}
if sys_prompt:
md["system_prompt"] = sys_prompt
if isinstance(ctx, dict):
md["runtime_context"] = ctx
yield build(
roomName=stable_id(slug, dedup),
agentId=agent_id,
memoryEntries=memory,
currentMessage=current,
expectedResponse=target,
availableActions=avail,
task_type="scam_defense",
source_dataset=slug,
license=license,
split=split,
extra_metadata=md,
)
# ─────────────────────────── n8n workflow generation ───────────────────────
#
# The runtime entry point is `parseWorkflowResponse()` in
# `@elizaos/plugin-n8n-workflow/dist/utils/generation.js`: it strips the
# leading ```json … ``` markdown fence and JSON.parses the body. The
# planner-level XML envelope (`<response>…</response>` wrapping a
# CREATE_WORKFLOW action) is the higher-level form the agent emits when it
# is choosing between actions — the JSON is then carried inline inside
# `<params>`. We emit BOTH shapes (50/50, deterministic by record index)
# so the SFT target distribution covers either form.
#
# `expectedResponse` is raw text. The workflow JSON is
# already structured and often multi-KB; double-encoding bloats tokens.
# Action surface the planner sees on n8n turns.
N8N_WORKFLOW_ACTIONS = ["CREATE_WORKFLOW", "PREVIEW_WORKFLOW", "REPLY", "IGNORE"]
def _n8n_synth_prompt_from_workflow(wf: dict[str, Any]) -> str:
"""Build a synthetic user prompt from a raw n8n workflow JSON."""
name = (wf.get("name") or "").strip() or "an n8n workflow"
nodes = wf.get("nodes") or []
types = []
for n in nodes if isinstance(nodes, list) else []:
t = (n.get("type") if isinstance(n, dict) else None) or ""
if t and t not in types:
types.append(t)
if len(types) >= 12:
break
integrations = ", ".join(types[:12]) if types else "various nodes"
return (
f"Generate the JSON for an n8n workflow named '{name}' that uses "
f"these node types: {integrations}. Return only the workflow JSON."
)
def _n8n_first_str(d: dict[str, Any], keys: list[str]) -> str:
for k in keys:
v = d.get(k)
if isinstance(v, str) and v.strip():
return v
return ""
_N8N_FENCE_RE = re.compile(r"```(?:json)?\s*\n?([\s\S]*?)\n?```", re.IGNORECASE)
def _n8n_extract_workflow(text: str) -> dict[str, Any] | None:
"""Return a workflow dict (must contain `nodes` + `connections`) parsed
from `text`, or None if no valid workflow can be recovered.
Tolerates raw JSON, ```json fenced JSON, and prose-prefixed JSON (the
`<thinking>…JSON…` shape used by stmasson and the markdown analysis
shape used by davidrpatton).
"""
if not isinstance(text, str) or not text.strip():
return None
body = text.strip()
# 1) Direct JSON.
try:
wf = json.loads(body)
if isinstance(wf, dict) and isinstance(wf.get("nodes"), list) \
and isinstance(wf.get("connections"), dict):
return wf
except json.JSONDecodeError:
pass
# 2) Fenced JSON block(s) — pick the first that yields a valid workflow.
for m in _N8N_FENCE_RE.finditer(body):
chunk = m.group(1).strip()
try:
wf = json.loads(chunk)
except json.JSONDecodeError:
continue
if isinstance(wf, dict) and isinstance(wf.get("nodes"), list) \
and isinstance(wf.get("connections"), dict):
return wf
# 3) First `{` … last `}` window. Useful for `<thinking>…{…}` shapes.
first = body.find("{")
last = body.rfind("}")
if first >= 0 and last > first:
try:
wf = json.loads(body[first:last + 1])
except json.JSONDecodeError:
return None
if isinstance(wf, dict) and isinstance(wf.get("nodes"), list) \
and isinstance(wf.get("connections"), dict):
return wf
return None
def _n8n_planner_target(wf: dict[str, Any]) -> dict[str, Any]:
"""Build the canonical elizaOS planner output for a CREATE_WORKFLOW
action, as a Python dict ready to encode.
Shape mirrors the `<response>` XML envelope nubilio emits — `thought`,
`actions[]{name, params}`, `providers[]`, `text`, `simple` — so the
student model learns one envelope across both runtime tasks and
workflow generation.
"""
name = (wf.get("name") or "").strip() or "untitled"
nodes = wf.get("nodes") if isinstance(wf.get("nodes"), list) else []
n_count = len(nodes)
trigger = ""
sink = ""
for n in nodes:
if not isinstance(n, dict):
continue
t = (n.get("type") or "").lower()
if not trigger and ("trigger" in t or "webhook" in t or t.endswith("formtrigger")):
trigger = n.get("name") or t.split(".")[-1] or "trigger"
if "googlesheets" in t or "telegram" in t or "slack" in t or "notion" in t \
or "gmail" in t or "discord" in t or "airtable" in t:
sink = n.get("name") or t.split(".")[-1] or sink
if not trigger:
trigger = "trigger"
if not sink:
if nodes and isinstance(nodes[-1], dict):
last = nodes[-1]
sink = last.get("name") or (last.get("type") or "").split(".")[-1] or "action"
else:
sink = "action"
return {
"thought": (
f"User wants a {trigger} to {sink} workflow. "
f"Drafting with {n_count} nodes."
),
"actions": [{
"name": "CREATE_WORKFLOW",
"params": {"workflow": wf},
}],
"providers": [],
"text": (
f"Drafted '{name}' with {n_count} nodes. Connect any required "
f"credentials, then confirm to deploy."
),
"simple": False,
}
def n8n_workflow(records, *, slug, license, split, encoder):
"""Universal adapter for n8n workflow-generation datasets.
Detects six common input shapes and emits one ElizaRecord per row with
`task_type='n8n_workflow_generation'`. The supervised target is the
elizaOS planner envelope encoded with the configured expected-response encoder — `thought`, `actions[]
{name, params:{workflow}}`, `providers[]`, `text`, `simple` — matching
nubilio's runtime planner output exactly.
Input shapes handled:
A) {messages:[{role,content},...]} — OpenAI/SFT
B) {prompt|instruction|input, json|answer|output|completion[, thinking]}
C) {workflow_json, workflow_name, integrations, ...} — Ker102 master
D) {name, nodes, connections} — batuhanilgarr
E) {key, value} — 0xarchit kv
F) {prompt, chosen, rejected, ...} — DPO (uses chosen)
"""
seen: set[str] = set()
emitted = 0
for r in records:
if not isinstance(r, dict):
continue
prompt_text: str = ""
target_text: str = ""
memory: list[dict[str, Any]] = []
sys_prompt: str = ""
thinking: str = ""
# Shape A: messages list
msgs = r.get("messages") or r.get("conversations")
if isinstance(msgs, list) and msgs:
sys_prompt, memory, current, final = _split_history(msgs)
if not current or not final:
continue
prompt_text = current.get("content", "")
target_text = final.get("content", "") or ""
# Shape F: DPO triples
elif isinstance(r.get("chosen"), str) and r.get("prompt"):
prompt_text = str(r.get("prompt") or "")
target_text = str(r.get("chosen") or "")
# Shape C: workflow_json + metadata (Ker102 master)
elif r.get("workflow_json"):
wf_raw = r.get("workflow_json")
target_text = wf_raw if isinstance(wf_raw, str) else json.dumps(wf_raw)
name = (r.get("workflow_name") or "").strip()
integrations = r.get("integrations") or ""
category = r.get("category") or ""
if isinstance(integrations, str) and integrations.startswith("["):
try:
integrations = ", ".join(json.loads(integrations))
except json.JSONDecodeError:
pass
prompt_text = (
f"Generate the JSON for an n8n workflow named '{name or 'untitled'}'"
+ (f" in the '{category}' category" if category else "")
+ (f" using these integrations: {integrations}" if integrations else "")
+ ". Return only the workflow JSON."
)
# Shape D: nodes + connections (batuhanilgarr)
elif r.get("nodes") is not None and r.get("connections") is not None:
nodes_v = r.get("nodes")
conns_v = r.get("connections")
try:
nodes_obj = json.loads(nodes_v) if isinstance(nodes_v, str) else nodes_v
conns_obj = json.loads(conns_v) if isinstance(conns_v, str) else conns_v
except json.JSONDecodeError:
continue
wf = {"name": r.get("name") or "", "nodes": nodes_obj, "connections": conns_obj}
prompt_text = _n8n_synth_prompt_from_workflow(wf)
target_text = json.dumps(wf, ensure_ascii=False, separators=(",", ":"))
# Shape E: kv (0xarchit)
elif isinstance(r.get("key"), str) and isinstance(r.get("value"), str):
prompt_text = r.get("key") or ""
target_text = r.get("value") or ""
# Shape B: prompt-completion pair
else:
instruction = _n8n_first_str(r, ["instruction"])
inp = _n8n_first_str(r, ["input"])
prompt_only = _n8n_first_str(r, ["prompt", "question", "query"])
target_text = _n8n_first_str(
r, ["json", "answer", "output", "completion", "response"]
)
if not target_text:
continue
if instruction and inp:
prompt_text = f"{instruction}\n\n{inp}".strip()
elif instruction:
prompt_text = instruction
elif prompt_only:
prompt_text = prompt_only
elif inp:
prompt_text = inp
else:
continue
t = r.get("thinking")
if isinstance(t, str) and t.strip():
thinking = t
if not prompt_text or not target_text:
continue
# Skip workflow-analysis tasks (image→description) that get
# mis-tagged as generation. The davidrpatton dataset is the main
# offender — its prompts begin with `<image>` and the assistant
# output is a prose description that happens to embed the workflow.
if prompt_text.lstrip().startswith("<image>"):
continue
# Recover a real workflow object from the raw target. This collapses
# the heterogeneous source shapes (raw JSON, fenced JSON, prose-
# prefixed JSON, French `<thinking>` chains-of-thought) into a single
# canonical structure we can re-emit deterministically.
wf = _n8n_extract_workflow(target_text)
if wf is None:
continue
dedup = stable_id(slug, prompt_text[:512], json.dumps(wf, sort_keys=True)[:512])
if dedup in seen:
continue
seen.add(dedup)
response = encoder.encode(_n8n_planner_target(wf))
response_shape = "structured_envelope"
emitted += 1
current_msg = {
"role": "user",
"speaker": "user",
"content": prompt_text,
"channel": "dm",
}
md: dict[str, Any] = {
"original_id": str(r.get("id") or r.get("workflow_id") or dedup),
"response_shape": response_shape,
}
if sys_prompt:
md["system_prompt"] = sys_prompt
if thinking:
md["thinking"] = thinking
# Carry over a few useful columns when present
for k in ("category", "complexity", "node_count", "integrations",
"source_url", "source_title", "workflow_name"):
v = r.get(k)
if v not in (None, "", []):
md[k] = v if not isinstance(v, (dict, list)) else json.dumps(v)
yield build(
roomName=stable_id(slug, prompt_text[:120]),
agentId="agent",
memoryEntries=memory,
currentMessage=current_msg,
expectedResponse=response,
availableActions=N8N_WORKFLOW_ACTIONS.copy(),
task_type="n8n_workflow_generation",
source_dataset=slug,
license=license,
split=split,
extra_metadata=md,
)
# ───────────────────────── dialogue datasets (raw) ──────────────────────────
def dialogue_raw(records, *, slug, license, split, encoder):
"""Raw chat datasets (Discord/Telegram). The normalizer treats these as
*unmolded* multi-turn corpora — we don't build supervised records here.
Instead, the dialogue routing synthesizer reads `data/raw/<slug>/` later
to mix conversations and label RESPOND/IGNORE turns. Yield nothing.
"""
if False:
yield # type: ignore[unreachable]
return
# ────────────────────── Facebook LIGHT / multilight ───────────────────────
# Memory window for routing/reply records (matches the multiparty synthesizer).
_LIGHT_MEMORY_WINDOW = 12
_LIGHT_PRIMARY_CONTEXT = "light-fantasy-roleplay"
def _light_has_addressing(text: str, speaker: str) -> bool:
"""Does `text` directly address `speaker`? (mention / leading vocative /
standalone name token)."""
if not text or not speaker:
return False
s = speaker.strip().lower()
if not s or s in {"user", "human", "ai", "assistant", "bot"}:
return False
t = text.lower()
if f"@{s}" in t:
return True
if re.search(rf"^\s*{re.escape(s)}\s*[,:?!\.]", t, re.I):
return True
if re.search(rf"\b{re.escape(s)}\b", t, re.I):
return True
return False
def _light_persona_for(characters: list[dict[str, Any]], name: str) -> str:
for ch in characters or []:
if (ch.get("name") or "").lower() == (name or "").lower():
persona = (ch.get("persona") or "").strip()
desc = (ch.get("desc") or "").strip()
if persona and desc and persona != desc:
return f"{persona}\n\n{desc}"
return persona or desc
return ""
def _light_memory_from_turns(
turns: list[dict[str, Any]],
) -> list[dict[str, Any]]:
return [
{
"role": "user",
"speaker": (t.get("speaker") or "user")[:60],
"content": (t.get("text") or "")[:2000],
"channel": "public",
}
for t in turns
if (t.get("text") or "").strip()
]
def light_multilight(records, *, slug, license, split, encoder):
"""Facebook LIGHT MultiLIGHT — multi-party fantasy text-adventure dialogues.
Source schema (one conversation per JSONL line, produced by our preproc
of the upstream EpisodeDB tarball):
{
"episode_id": "EPI-…",
"split": "train" | "validation" | "test",
"location": {"name", "description", "extra_desc"},
"characters": [{"name", "persona", "desc"} × 3],
"messages": [{"speaker", "text", "timestamp"} …]
}
Each conversation has exactly 3 named characters speaking in turn. For
every message at index i ≥ 1 we walk it from the perspective of the
speaker at index i (the "agent") and emit:
- one `should_respond_with_context` record where the latest other
character's turn is `currentMessage` and the agent decides
RESPOND (because it actually spoke next) — yielding a positive
routing label.
- one `should_respond_with_context` record from the perspective of
each *other* character at the same point: their `currentMessage`
is the same latest other-character turn, and their target action
is IGNORE (they did NOT speak next). This yields negatives without
synthesis.
- one `reply` record for the agent that actually spoke, training the
model to produce the exact line the corpus shows.
All routing targets render as the canonical structured document
`{name, reasoning, action, primaryContext, secondaryContexts,
evidenceTurnIds}`; reply targets render as `{thought, text}`.
"""
for r in records:
if not isinstance(r, dict):
continue
messages = r.get("messages") or []
characters = r.get("characters") or []
if len(messages) < 2 or not characters:
continue
episode_id = r.get("episode_id") or stable_id(slug, json.dumps(messages[:1]))
location = r.get("location") or {}
location_name = (location.get("name") or "").strip()
location_desc = (location.get("description") or "").strip()
rec_split = r.get("split") or split or "train"
all_speakers = [
(ch.get("name") or "").strip()
for ch in characters
if (ch.get("name") or "").strip()
]
if not all_speakers:
continue
for i in range(1, len(messages)):
spoken = messages[i]
actual_speaker = (spoken.get("speaker") or "").strip()
actual_text = (spoken.get("text") or "").strip()
if not actual_speaker or not actual_text:
continue
# The latest non-actual-speaker turn before i is what the agent
# is "responding to". For multi-party we just take messages[i-1]
# — that's the conversation as played out.
prev = messages[i - 1]
prev_speaker = (prev.get("speaker") or "").strip()
prev_text = (prev.get("text") or "").strip()
if not prev_speaker or not prev_text:
continue
# Skip if the previous message is from the same speaker — the
# multiparty pattern needs a different "current" speaker.
if prev_speaker.lower() == actual_speaker.lower():
continue
window_start = max(0, (i - 1) - _LIGHT_MEMORY_WINDOW)
context_turns = messages[window_start : i - 1]
current_msg = {
"role": "user",
"speaker": prev_speaker[:60],
"content": prev_text[:2000],
"channel": "public",
}
memory = _light_memory_from_turns(context_turns)
# ------ Positive routing record (the speaker that actually spoke)
agent_persona = _light_persona_for(characters, actual_speaker)
addressed = _light_has_addressing(prev_text, actual_speaker)
reasoning = (
f"{actual_speaker} is named/addressed in the prior turn, so "
"they should reply."
if addressed
else f"It is {actual_speaker}'s turn in the conversation, so "
"they should reply."
)
target = {
"name": actual_speaker,
"reasoning": reasoning,
"action": ACTION_RESPOND,
"primaryContext": _LIGHT_PRIMARY_CONTEXT,
"secondaryContexts": location_name,
"evidenceTurnIds": "",
}
md_routing: dict[str, Any] = {
"episode_id": episode_id,
"agent_name": actual_speaker,
"synth_target_action": ACTION_RESPOND,
"task_type_handler": "should_respond",
"addressed_by_name": addressed,
"location_name": location_name,
"num_speakers": len(all_speakers),
}
if agent_persona:
md_routing["persona"] = agent_persona
if location_desc:
md_routing["location_description"] = location_desc[:500]
yield build(
roomName=stable_id(slug, episode_id, i, "respond", actual_speaker),
agentId=actual_speaker.lower(),
memoryEntries=memory,
currentMessage=current_msg,
expectedResponse=encoder.encode(target),
availableActions=ROUTING_ACTIONS.copy(),
task_type="should_respond_with_context",
source_dataset=slug,
license=license,
split=rec_split,
extra_metadata=md_routing,
)
# ------ Negative routing records: each other named character
# who did NOT speak at turn i. Ground-truth IGNORE label.
for other in all_speakers:
if other.lower() == actual_speaker.lower():
continue
if other.lower() == prev_speaker.lower():
# The prior speaker isn't expected to immediately respond
# to themselves; conventionally we still yield this as
# IGNORE, but skip to keep records cleaner — they just
# spoke.
continue
other_persona = _light_persona_for(characters, other)
other_addressed = _light_has_addressing(prev_text, other)
other_reasoning = (
f"{other} is not named or addressed in the prior turn, "
f"and {actual_speaker} is the one taking the turn."
if not other_addressed
else f"Although {other} could plausibly speak, "
f"{actual_speaker} takes this turn instead."
)
neg_target = {
"name": other,
"reasoning": other_reasoning,
"action": ACTION_IGNORE,
"primaryContext": _LIGHT_PRIMARY_CONTEXT,
"secondaryContexts": location_name,
"evidenceTurnIds": "",
}
md_neg: dict[str, Any] = {
"episode_id": episode_id,
"agent_name": other,
"synth_target_action": ACTION_IGNORE,
"task_type_handler": "should_respond",
"addressed_by_name": other_addressed,
"location_name": location_name,
"num_speakers": len(all_speakers),
"actual_speaker": actual_speaker,
}
if other_persona:
md_neg["persona"] = other_persona
if location_desc:
md_neg["location_description"] = location_desc[:500]
yield build(
roomName=stable_id(slug, episode_id, i, "ignore", other),
agentId=other.lower(),
memoryEntries=memory,
currentMessage=current_msg,
expectedResponse=encoder.encode(neg_target),
availableActions=ROUTING_ACTIONS.copy(),
task_type="should_respond_with_context",
source_dataset=slug,
license=license,
split=rec_split,
extra_metadata=md_neg,
)
# ------ Reply record for the agent that actually spoke. The
# supervised target is `{thought, text}` rendered with the configured expected-response encoder.
reply_target = {
"thought": (
f"As {actual_speaker}, I respond to {prev_speaker} in "
f"{location_name}." if location_name
else f"As {actual_speaker}, I respond to {prev_speaker}."
),
"text": actual_text,
}
md_reply: dict[str, Any] = {
"episode_id": episode_id,
"agent_name": actual_speaker,
"task_type_handler": "reply",
"location_name": location_name,
"num_speakers": len(all_speakers),
}
if agent_persona:
md_reply["persona"] = agent_persona
if location_desc:
md_reply["location_description"] = location_desc[:500]
yield build(
roomName=stable_id(slug, episode_id, i, "reply", actual_speaker),
agentId=actual_speaker.lower(),
memoryEntries=memory,
currentMessage=current_msg,
expectedResponse=encoder.encode(reply_target),
availableActions=REPLY_ACTIONS.copy(),
task_type="reply",
source_dataset=slug,
license=license,
split=rec_split,
extra_metadata=md_reply,
)
# ──────────────────────────── claude distillation ──────────────────────────
# System prompt used for Claude-distilled records when the upstream `system`
# turn is empty (which is the common case in `Kassadin88/Claude-Distills`).
# The wording is intentionally minimal — we want the model to learn the
# `<think>...</think>final` shape from the data, not to memorize a long
# header. When the upstream record DOES carry a system message, we use it
# verbatim instead.
CLAUDE_DISTILL_SYSTEM = (
"You are a helpful, careful assistant. Think step by step inside "
"<think>...</think> tags before producing your final answer."
)
def claude_distill(records: Iterator[dict], *, slug: str, license: str,
split: str, encoder: ExpectedResponseEncoder) -> Iterator[ElizaRecord]:
"""Adapter for Kassadin88/Claude-Distills (and similarly-shaped distill
corpora). Each record is `{messages: [system?, user, assistant], source}`
and the assistant content already contains
`<think>{reasoning}</think>{final answer}`.
We preserve the assistant content **verbatim** in `expectedResponse`
without re-encoding so the student model learns the exact `<think>`
surface that the active reasoning generation pipeline expects.
The `messages` array is rendered into `memoryEntries` + `currentMessage`
+ `expectedResponse` so `tokenizer.apply_chat_template(...)` produces
a chat that is byte-uniform with the upstream distill.
"""
for r in records:
msgs = r.get("messages") or []
if not isinstance(msgs, list) or not msgs:
continue
system_parts: list[str] = []
convo: list[dict[str, Any]] = []
for m in msgs:
if not isinstance(m, dict):
continue
role = _norm_role(m.get("role") or "")
content = m.get("content") or ""
if isinstance(content, list):
content = "".join(
p.get("text", "") if isinstance(p, dict) else str(p)
for p in content
)
content = _strip_surrogates(str(content))
if role == "system":
if content.strip():
system_parts.append(content)
continue
if role not in ("user", "assistant"):
continue
convo.append({"role": role, "content": content})
# Need at least one user turn and one assistant turn — the supervised
# target is the final assistant turn.
final_assistant = None
for i in range(len(convo) - 1, -1, -1):
if convo[i]["role"] == "assistant":
final_assistant = convo[i]
final_idx = i
break
if final_assistant is None or not (final_assistant["content"] or "").strip():
continue
prior = convo[:final_idx]
current = None
for m in reversed(prior):
if m["role"] == "user" and (m["content"] or "").strip():
current = {
"role": "user", "speaker": "user",
"content": m["content"], "channel": "dm",
}
prior.remove(m)
break
if current is None:
continue
memory = [
{"role": m["role"], "speaker": m["role"],
"content": m["content"], "channel": "dm"}
for m in prior
]
sys_prompt = "\n\n".join(system_parts).strip() or CLAUDE_DISTILL_SYSTEM
source = str(r.get("source") or "")
md = {
"system_prompt": sys_prompt,
"claude_source": source,
"preserve_think_tags": True,
}
seed = current["content"][:160] + "|" + (final_assistant["content"][:80])
yield build(
roomName=stable_id(slug, source, seed),
agentId="assistant",
memoryEntries=memory,
currentMessage=current,
# Verbatim. The `<think>...</think>final`
# surface ships through `tokenizer.apply_chat_template` exactly
# as the distill source recorded it.
expectedResponse=final_assistant["content"],
# Intentionally empty — Claude distills are general-purpose Q&A,
# not elizaOS action routing. Empty list prevents the
# "Available actions: ..." suffix from being appended to the
# system prompt by format_for_training.py.
availableActions=[],
task_type="claude_distill",
source_dataset=slug,
license=license,
split=split,
extra_metadata=md,
)
# ────────────────── abliteration calibration corpora ─────────────────────
#
# These adapters consume `mlabonne/harmful_behaviors` and
# `mlabonne/harmless_alpaca` (or any equivalent benign-instruction set).
# The output is NOT a supervised target — it's calibration data for the
# orthogonal-projection refusal-direction ablation in
# `scripts/quantization/abliteration_apply.py`. The downstream consumer
# only reads `currentMessage.content`; `expectedResponse` carries a
# sentinel so `ElizaRecord.is_valid()` accepts the row.
#
# `pack_dataset.py` filters records with task_type in
# {"abliteration_harmful","abliteration_harmless"} out of train/val/test
# and writes them to `data/abliteration/{harmful,harmless}.jsonl`.
_ABLITERATION_PROMPT_KEYS = (
"prompt", "goal", "instruction", "text", "behavior", "input", "question",
)
_ABLITERATION_SENTINEL = "<abliteration-calibration>"
def _abliteration_prompt(rec: dict[str, Any]) -> str:
for key in _ABLITERATION_PROMPT_KEYS:
val = rec.get(key)
if isinstance(val, str) and val.strip():
return val.strip()
msgs = rec.get("messages") or rec.get("conversations")
if isinstance(msgs, list):
for m in msgs:
if not isinstance(m, dict):
continue
if _norm_role(str(m.get("role") or m.get("from") or "")) == "user":
content = m.get("content") or m.get("value") or ""
if isinstance(content, str) and content.strip():
return content.strip()
return ""
def _abliteration_yield(
records, *, slug, license, split, task_type, channel,
):
for r in records:
if not isinstance(r, dict):
continue
prompt = _abliteration_prompt(r)
if not prompt:
continue
prompt = _strip_surrogates(prompt)[:4000]
yield build(
roomName=stable_id(slug, task_type, prompt[:160]),
agentId="calibration",
currentMessage={
"role": "user", "speaker": "user",
"content": prompt, "channel": channel,
},
expectedResponse=_ABLITERATION_SENTINEL,
availableActions=[],
task_type=task_type,
source_dataset=slug,
license=license,
split=split,
extra_metadata={"abliteration_calibration": True},
)
def harmful_behaviors(records, *, slug, license, split, encoder):
"""mlabonne/harmful_behaviors — refusal-eliciting prompts. Calibration
only: emits ElizaRecord with task_type=abliteration_harmful and a
sentinel expectedResponse. Routed to data/abliteration/harmful.jsonl
by pack_dataset.py (weight=0.0 in datasets.yaml)."""
yield from _abliteration_yield(
records, slug=slug, license=license, split=split,
task_type="abliteration_harmful", channel="abliteration",
)
def harmless_alpaca(records, *, slug, license, split, encoder):
"""mlabonne/harmless_alpaca — paired benign instructions for
orthogonal-projection abliteration. Calibration only: emits
ElizaRecord with task_type=abliteration_harmless and a sentinel
expectedResponse. Routed to data/abliteration/harmless.jsonl by
pack_dataset.py (weight=0.0 in datasets.yaml)."""
yield from _abliteration_yield(
records, slug=slug, license=license, split=split,
task_type="abliteration_harmless", channel="abliteration",
)
# ──────────────────────────────── registry ─────────────────────────────────
REGISTRY: dict[str, Adapter] = {
# core / canonical
"scambench_passthrough": scambench_passthrough,
# tool calling
"hermes_fc": hermes_fc,
"hermes_fc_thinking": hermes_fc_thinking,
"glaive_fc": glaive_fc,
"glaive_fc_reasoning": glaive_fc_reasoning,
"sharegpt_tool_calls": sharegpt_tool_calls,
"functions_53k": functions_53k,
"bitagent": bitagent,
"toolhop": toolhop,
# operator / mobile
"openclaw_operator": openclaw_operator,
"mobile_actions": mobile_actions,
# agentic / hermes traces
"nemotron_rl_tool_use": nemotron_rl_tool_use,
"qwen36_trajectory": qwen36_trajectory,
"hermes_reasoning_tool_use": hermes_reasoning_tool_use,
"dolci_instruct": dolci_instruct,
"hermes_traces": hermes_traces,
"hermes_omniforge": hermes_omniforge,
"hermes_3": hermes_3,
"aureth": aureth,
"nemotron_coding_reasoning": nemotron_coding_reasoning,
"hf_coding_tools_traces": hf_coding_tools_traces,
"chatml_text": chatml_text,
"gemma_text": gemma_text,
"open_paws_llama": open_paws_llama,
"noesis_text": noesis_text,
# MCP
"mcp_messages": mcp_messages,
"mcp_routing": mcp_routing,
"mcp_flow": mcp_flow,
# shell / terminal / agent trajectories
"terminal_corpus": terminal_corpus,
"agent_trove": agent_trove,
# reasoning / CoT
"reasoning_cot": reasoning_cot,
# raw dialogue (consumed by synthesize_routing.py)
"dialogue_raw": dialogue_raw,
# multi-party fantasy roleplay (Facebook LIGHT MultiLIGHT)
"light_multilight": light_multilight,
# local eliza corpora
"nubilio_trajectories": nubilio_trajectories,
"scam_defense_corpus": scam_defense_corpus,
# Nightly trajectory-export bridge (TS app-training plugin → Python
# training pipeline). Rows are already eliza_native_v1 and have been
# through the TS privacy filter; the passthrough adapter validates the
# format and re-emits the canonical ElizaRecord intermediate.
"eliza_native_passthrough": eliza_native_passthrough,
# n8n workflow generation
"n8n_workflow": n8n_workflow,
# Claude distillation (Kassadin88/Claude-Distills) — preserves
# <think>…</think>final-answer in expectedResponse verbatim.
"claude_distill": claude_distill,
# Abliteration calibration corpora (NOT in train mix; weight=0.0).
# pack_dataset.py routes these to data/abliteration/{harmful,harmless}.jsonl.
"harmful_behaviors": harmful_behaviors,
"harmless_alpaca": harmless_alpaca,
}