"""Generate multi-turn tool-calling agent SFT dataset with verified SciPy execution.""" from __future__ import annotations import json import random import sys from pathlib import Path from typing import Any PROJECT_ROOT = Path(__file__).resolve().parents[1] if str(PROJECT_ROOT) not in sys.path: sys.path.insert(0, str(PROJECT_ROOT)) import numpy as np from scipy import linalg, signal from controlai_agent.registry import registry import controlai_agent.tools # Register tools OUTPUT_DIR = Path("data/training/agent_sft_v1") RNG_SEED = 20260817 def format_matrix_latex(mat: np.ndarray) -> str: if mat.ndim == 1: return "[" + ", ".join(f"{x:.4g}" for x in mat) + "]" rows = [] for r in mat: rows.append(" & ".join(f"{x:.4g}" for x in r)) return "\\begin{bmatrix} " + " \\\\ ".join(rows) + " \\end{bmatrix}" def generate_zoh_trajectory(rng: random.Random) -> dict[str, Any]: n = rng.choice([2, 3]) A = np.diag([-rng.uniform(0.5, 3.0) for _ in range(n)]) for i in range(n - 1): A[i, i + 1] = rng.uniform(0.5, 2.0) B = np.zeros((n, 1)) B[-1, 0] = rng.uniform(1.0, 3.0) Ts = rng.choice([0.01, 0.02, 0.05, 0.1, 0.2]) A_list = A.tolist() B_list = B.tolist() prompt = f"Discretize the continuous-time state-space system with A={A_list} and B={B_list} using exact zero-order hold at sampling period Ts={Ts} s." tool_call_text = f""" {{"name": "exact_zoh", "arguments": {{"A": {A_list}, "B": {B_list}, "Ts": {Ts}}}}} """ result = registry.execute("exact_zoh", {"A": A_list, "B": B_list, "Ts": Ts}) Ad = np.array(result["Ad"]) Bd = np.array(result["Bd"]) assistant_final = f"""The continuous-time system has been discretized under exact Zero-Order Hold (ZOH) at sample time $T_s = {Ts}$ s. The resulting discrete-time state-space matrices are: $$ A_d = {format_matrix_latex(Ad)}, \\quad B_d = {format_matrix_latex(Bd)} $$ The discretization was cross-verified using the block matrix exponential method with a residual tolerance below $10^{{-7}}$. The system is ready for discrete-time state estimation or controller synthesis.""" return { "messages": [ {"role": "user", "content": prompt}, {"role": "assistant", "content": tool_call_text}, {"role": "tool", "name": "exact_zoh", "content": json.dumps(result)}, {"role": "assistant", "content": assistant_final}, ], "metadata": {"task_type": "tool_calling", "family": "exact_zoh", "domain": "linear_systems"}, } def generate_lqr_trajectory(rng: random.Random) -> dict[str, Any]: A = [[0.0, 1.0], [-rng.uniform(1.0, 5.0), -rng.uniform(1.0, 4.0)]] B = [[0.0], [rng.uniform(1.0, 3.0)]] q1 = rng.choice([1.0, 5.0, 10.0, 20.0]) q2 = rng.choice([0.1, 1.0, 2.0]) Q = [[q1, 0.0], [0.0, q2]] r_val = rng.choice([0.1, 1.0, 2.0]) R = [[r_val]] prompt = f"Design a continuous-time LQR state feedback controller for system A={A}, B={B} with state penalty Q={Q} and control penalty R={R}. Compute the algebraic Riccati solution P, feedback gain K, and closed-loop poles." tool_call_text = f""" {{"name": "continuous_lqr", "arguments": {{"A": {A}, "B": {B}, "Q": {Q}, "R": {R}}}}} """ result = registry.execute("continuous_lqr", {"A": A, "B": B, "Q": Q, "R": R}) P_mat = np.array(result["P"]) K_mat = np.array(result["K"]) assistant_final = f"""The continuous-time Linear Quadratic Regulator (LQR) has been synthesized by solving the Algebraic Riccati Equation (CARE): 1. **Riccati Solution Matrix $P$**: $$ P = {format_matrix_latex(P_mat)} $$ 2. **Optimal State Feedback Gain $K$**: $$ K = {format_matrix_latex(K_mat)} $$ 3. **Closed-Loop Poles**: $$ {result['closed_loop_poles']} $$ The Riccati residual is verified at {result['verification']['riccati_residual_max']:.2e}, and all closed-loop eigenvalues lie strictly in the open left-half plane, certifying closed-loop asymptotic stability.""" return { "messages": [ {"role": "user", "content": prompt}, {"role": "assistant", "content": tool_call_text}, {"role": "tool", "name": "continuous_lqr", "content": json.dumps(result)}, {"role": "assistant", "content": assistant_final}, ], "metadata": {"task_type": "tool_calling", "family": "continuous_lqr", "domain": "optimal_control"}, } def generate_cbf_trajectory(rng: random.Random) -> dict[str, Any]: x_min = -rng.uniform(0.5, 2.0) x = x_min + rng.uniform(0.1, 0.5) u_nom = -rng.uniform(1.5, 4.0) alpha = rng.choice([0.5, 1.0, 1.2, 1.5, 2.0]) prompt = f"For a scalar single-integrator system x_dot = u with state safety boundary x >= {x_min:.2f}, apply a Control Barrier Function safety filter with class-K gain alpha = {alpha:.1f}. Given nominal control u_nom = {u_nom:.2f} at state x = {x:.2f}, compute the safe control input u." tool_call_text = f""" {{"name": "cbf_safety_filter", "arguments": {{"x": {x:.4f}, "u_nom": {u_nom:.4f}, "alpha": {alpha:.1f}, "x_min": {x_min:.4f}}}}} """ result = registry.execute("cbf_safety_filter", {"x": x, "u_nom": u_nom, "alpha": alpha, "x_min": x_min}) assistant_final = f"""The Control Barrier Function (CBF) quadratic program safety filter yields: - **Barrier Function Value**: $h(x) = x - x_{{min}} = {result['h']:.4f}$ - **Lower Admissible Bound**: $\\dot{{h}} + \\alpha h \\ge 0 \\implies u \\ge -\\alpha h = {result['cbf_lower_bound']:.4f}$ - **Filtered Safe Control Input $u^*$**: ${result['u_safe']:.4f}$ The safety constraint is {'active' if result['is_constraint_active'] else 'inactive'}. The filtered control $u^*$ guarantees forward invariance of the safe set $\\mathcal{{C}} = \\{{x \\mid x \\ge {x_min:.2f}\\}}$, minimally perturbing the nominal control $u_{{nom}} = {u_nom:.2f}$.""" return { "messages": [ {"role": "user", "content": prompt}, {"role": "assistant", "content": tool_call_text}, {"role": "tool", "name": "cbf_safety_filter", "content": json.dumps(result)}, {"role": "assistant", "content": assistant_final}, ], "metadata": {"task_type": "tool_calling", "family": "cbf_safety_filter", "domain": "safety_critical"}, } def generate_underspecified_trajectory(rng: random.Random) -> dict[str, Any]: cases = [ ( "Design a discrete state-feedback controller for the continuous plant with A=[[0, 1], [-4, -2]], B=[[0], [1]].", "To design a discrete state-feedback controller, the following critical specifications are missing:\n1. **Sampling Period ($T_s$)**: Required to discretize the continuous-time plant matrices $(A, B)$ via Zero-Order Hold.\n2. **Performance Specifications**: Desired closed-loop pole locations $z_i$ or state/input penalty weights $(Q, R)$ for discrete LQR synthesis.\n\nPlease provide the sampling period $T_s$ and your desired closed-loop pole locations or LQR weighting matrices.", ), ( "Tune a PID controller for an unknown motor.", "To tune a PID controller, the mathematical model or empirical response of the motor is required:\n1. **Plant Transfer Function** $G(s)$ or State-Space Realization $(A, B, C, D)$.\n2. **Or Empirical FOPDT Parameters**: Process static gain $K$, time constant $\\tau$, and apparent dead time $L$.\n3. **Design Criteria**: Target phase margin, settling time, or overshoot tolerance.\n\nPlease provide the plant transfer function or experimental step response parameters.", ), ( "Solve the MPC quadratic program for initial state x0=[1.0, 0.0].", "To formulate and solve the Model Predictive Control (MPC) quadratic program, the following parameters are required:\n1. **Discrete System Dynamics**: State matrix $A$ and input matrix $B$.\n2. **Penalty Matrices**: State weighting matrix $Q$ and control weighting matrix $R$.\n3. **Prediction Horizon**: Horizon length $N$.\n4. **Actuator/State Constraints**: Bounds on inputs $|u_k| \\le u_{max}$ and states $x_{min} \\le x_k \\le x_{max}$.\n\nPlease provide the plant matrices $(A, B)$, weights $(Q, R)$, prediction horizon $N$, and any state/input constraints.", ), ] prompt, response = rng.choice(cases) return { "messages": [ {"role": "user", "content": prompt}, {"role": "assistant", "content": response}, ], "metadata": {"task_type": "underspecified", "family": "refusal", "domain": "specification_gating"}, } def main() -> int: rng = random.Random(RNG_SEED) generators = [ generate_zoh_trajectory, generate_lqr_trajectory, generate_cbf_trajectory, ] all_records = [] # 1. Tool calling trajectories for _ in range(1800): gen = rng.choice(generators) all_records.append(gen(rng)) # 2. Underspecified refusal trajectories for _ in range(300): all_records.append(generate_underspecified_trajectory(rng)) rng.shuffle(all_records) split_idx = int(0.85 * len(all_records)) train_records = all_records[:split_idx] valid_records = all_records[split_idx:] OUTPUT_DIR.mkdir(parents=True, exist_ok=True) with (OUTPUT_DIR / "train.jsonl").open("w", encoding="utf-8") as f: for r in train_records: f.write(json.dumps(r, ensure_ascii=False) + "\n") with (OUTPUT_DIR / "valid.jsonl").open("w", encoding="utf-8") as f: for r in valid_records: f.write(json.dumps(r, ensure_ascii=False) + "\n") print(f"Generated Agent SFT dataset in {OUTPUT_DIR}:") print(f" - Train: {len(train_records):,} records") print(f" - Valid: {len(valid_records):,} records") return 0 if __name__ == "__main__": raise SystemExit(main())