cace-env / cace_env /inference.py
Sunnycodes3114's picture
Upload folder using huggingface_hub
7e24fa5 verified
Raw
History Blame Contribute Delete
13.4 kB
"""
inference.py
Run inference against the CACE OpenEnv environment and plot reward curves.
Usage:
python inference.py
python inference.py --episodes 20 --model Sannidhay/cace-grpo-model
"""
import os, json, argparse, time
import requests
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
import numpy as np
from datetime import datetime
# ── Config ────────────────────────────────────────────────────────────────────
ENV_URL = os.environ.get("ENV_BASE_URL", "https://sannidhay-cace-env.hf.space")
HF_TOKEN = os.environ.get("HF_TOKEN", "")
ACTION_MAP = {
0: "ALLOW", 1: "REMOVE", 2: "ALLOW_WITH_LABEL",
3: "ESCALATE", 4: "RESTRICT_DISTRIBUTION",
}
ACTION_COLORS = {
"ALLOW": "#2ecc71", "REMOVE": "#e74c3c",
"ALLOW_WITH_LABEL": "#f39c12", "ESCALATE": "#9b59b6",
"RESTRICT_DISTRIBUTION": "#3498db",
}
# ── Simple LLM Decision Agent ─────────────────────────────────────────────────
def get_decision_from_model(observation: str, model: str = None) -> tuple[str, int]:
"""
Get moderation decision from model.
Uses SFT/GRPO model if available, falls back to rule-based.
"""
obs_upper = observation.upper()
# Rule-based fallback (works without GPU)
if "REMOVE" in obs_upper and ("HATE" in obs_upper or "VIOLENCE" in obs_upper or "HARM" in obs_upper):
if "CULTURAL" in obs_upper and "LEGITIMATE" in obs_upper:
return "ESCALATE", 3
return "REMOVE", 1
elif "ALLOW" in obs_upper and "CULTURAL" in obs_upper:
return "ALLOW", 0
elif "HIGH" in obs_upper and "COMPLEX" in obs_upper:
return "ESCALATE", 3
else:
return "ALLOW", 0
# ── OpenEnv client ────────────────────────────────────────────────────────────
class CACEClient:
def __init__(self, base_url: str):
self.base_url = base_url.rstrip("/")
self.session = requests.Session()
if HF_TOKEN:
self.session.headers["Authorization"] = f"Bearer {HF_TOKEN}"
def health(self) -> bool:
try:
r = self.session.get(f"{self.base_url}/health", timeout=15)
return r.status_code == 200 and r.json().get("status") == "ok"
except Exception as e:
print(f"[DEBUG] Health check failed: {e}")
return False
def wait_until_ready(self, max_wait: int = 120):
print(f"[DEBUG] Waiting for server at {self.base_url} ...")
for i in range(max_wait):
if self.health():
print(f"[DEBUG] Server is ready!")
return True
time.sleep(1)
if i % 10 == 9:
print(f"[DEBUG] Still waiting... ({i+1}s)")
raise RuntimeError(f"Server not ready after {max_wait}s")
def reset(self) -> str:
r = self.session.post(f"{self.base_url}/reset", timeout=60)
r.raise_for_status()
obs_r = self.session.get(f"{self.base_url}/observation", timeout=30)
return obs_r.json()["observation"]
def step(self, action_int: int) -> dict:
r = self.session.post(
f"{self.base_url}/step",
json={"action_int": action_int},
timeout=30,
)
r.raise_for_status()
return r.json()
def info(self) -> dict:
r = self.session.get(f"{self.base_url}/info", timeout=10)
return r.json()
def metrics(self) -> dict:
r = self.session.get(f"{self.base_url}/metrics", timeout=10)
return r.json()
# ── Run episodes ──────────────────────────────────────────────────────────────
def run_episodes(env: CACEClient, n_episodes: int, model: str = None) -> list[dict]:
results = []
for ep in range(1, n_episodes + 1):
obs = env.reset()
decision, action_int = get_decision_from_model(obs, model)
result = env.step(action_int)
reward = result.get("reward", 0.0)
done = result.get("done", True)
info = result.get("info", {})
ground_truth= info.get("ground_truth", "?")
correct = info.get("correct", decision == ground_truth)
language = info.get("language", "Unknown")
region = info.get("region", "Unknown")
breakdown = info.get("reward_breakdown", {})
ep_result = {
"episode": ep,
"decision": decision,
"ground_truth": ground_truth,
"reward": float(reward),
"correct": correct,
"done": done,
"language": language,
"region": region,
"t1_cultural": breakdown.get("track1_cultural", 0),
"t2_harm": breakdown.get("track2_harm", 0),
"t3_policy": breakdown.get("track3_policy", 0),
}
results.append(ep_result)
status = "✓" if correct else "✗"
print(
f"[STEP] ep={ep} decision={decision} gt={ground_truth} "
f"reward={reward:+.3f} correct={str(correct).lower()} {status} "
f"lang={language}"
)
return results
# ── Plotting ──────────────────────────────────────────────────────────────────
def plot_results(results: list[dict], save_path: str = "cace_inference_results.png"):
episodes = [r["episode"] for r in results]
rewards = [r["reward"] for r in results]
correct = [r["correct"] for r in results]
decisions= [r["decision"] for r in results]
# Running averages
window = min(5, len(results))
avg_rewards = np.convolve(rewards, np.ones(window)/window, mode='valid')
avg_correct = np.convolve([1 if c else 0 for c in correct], np.ones(window)/window, mode='valid')
fig = plt.figure(figsize=(16, 10))
fig.patch.set_facecolor('#0f1117')
gs = gridspec.GridSpec(2, 3, figure=fig, hspace=0.4, wspace=0.35)
GOLD = "#FFD700"
GREEN = "#2ecc71"
RED = "#e74c3c"
BLUE = "#3498db"
PURPLE = "#9b59b6"
BG = '#0f1117'
PANEL = '#1a1d2e'
def style_ax(ax, title):
ax.set_facecolor(PANEL)
ax.set_title(title, color=GOLD, fontsize=11, fontweight='bold', pad=8)
ax.tick_params(colors='white')
ax.xaxis.label.set_color('white')
ax.yaxis.label.set_color('white')
for spine in ax.spines.values():
spine.set_edgecolor('#333')
# ── Plot 1: Reward per episode ────────────────────────────────────────────
ax1 = fig.add_subplot(gs[0, :2])
style_ax(ax1, "Reward per Episode")
colors = [GREEN if r > 0 else RED for r in rewards]
ax1.bar(episodes, rewards, color=colors, alpha=0.7, label="Episode reward")
if len(avg_rewards) > 0:
x_avg = episodes[window-1:]
ax1.plot(x_avg, avg_rewards, color=GOLD, linewidth=2.5,
label=f"Rolling avg (n={window})", zorder=5)
ax1.axhline(0, color='white', linewidth=0.5, linestyle='--', alpha=0.3)
ax1.set_xlabel("Episode")
ax1.set_ylabel("Reward")
ax1.legend(facecolor=PANEL, labelcolor='white', fontsize=9)
ax1.set_ylim(-1.2, 1.2)
# ── Plot 2: Accuracy ──────────────────────────────────────────────────────
ax2 = fig.add_subplot(gs[0, 2])
style_ax(ax2, "Accuracy")
accuracy = sum(correct) / len(correct)
ax2.pie(
[accuracy, 1-accuracy],
labels=["Correct", "Wrong"],
colors=[GREEN, RED],
autopct='%1.0f%%',
textprops={'color': 'white', 'fontsize': 11},
startangle=90,
)
ax2.set_title(f"Accuracy\n{accuracy*100:.1f}% ({sum(correct)}/{len(correct)})",
color=GOLD, fontsize=11, fontweight='bold')
# ── Plot 3: Decision distribution ─────────────────────────────────────────
ax3 = fig.add_subplot(gs[1, 0])
style_ax(ax3, "Decision Distribution")
from collections import Counter
dec_counts = Counter(decisions)
labels = list(dec_counts.keys())
vals = list(dec_counts.values())
bar_colors = [ACTION_COLORS.get(l, BLUE) for l in labels]
bars = ax3.bar(labels, vals, color=bar_colors, alpha=0.85)
ax3.set_xticklabels(labels, rotation=20, ha='right', fontsize=8)
for bar, val in zip(bars, vals):
ax3.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.1,
str(val), ha='center', color='white', fontsize=9)
ax3.set_ylabel("Count")
# ── Plot 4: Three-track reward breakdown ──────────────────────────────────
ax4 = fig.add_subplot(gs[1, 1])
style_ax(ax4, "3-Track Reward Breakdown (avg)")
t1_avg = np.mean([r["t1_cultural"] for r in results])
t2_avg = np.mean([r["t2_harm"] for r in results])
t3_avg = np.mean([r["t3_policy"] for r in results])
tracks = ["Cultural\n(40%)", "Harm\n(35%)", "Policy\n(25%)"]
vals = [t1_avg, t2_avg, t3_avg]
bar_colors2 = [GOLD, PURPLE, BLUE]
bars2 = ax4.bar(tracks, vals, color=bar_colors2, alpha=0.85)
for bar, val in zip(bars2, vals):
ax4.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.5,
f"{val:.1f}", ha='center', color='white', fontsize=10)
ax4.set_ylabel("Score (0-100)")
ax4.set_ylim(0, 110)
# ── Plot 5: Running accuracy ───────────────────────────────────────────────
ax5 = fig.add_subplot(gs[1, 2])
style_ax(ax5, "Running Accuracy")
if len(avg_correct) > 0:
x_acc = episodes[window-1:]
ax5.plot(x_acc, avg_correct * 100, color=GREEN, linewidth=2.5)
ax5.fill_between(x_acc, avg_correct * 100, alpha=0.2, color=GREEN)
ax5.axhline(50, color='white', linewidth=0.5, linestyle='--', alpha=0.3)
ax5.set_xlabel("Episode")
ax5.set_ylabel("Accuracy (%)")
ax5.set_ylim(0, 105)
# ── Title ──────────────────────────────────────────────────────────────────
fig.suptitle(
"CACE — Cultural Context Arbitration Environment\nInference Results",
color=GOLD, fontsize=14, fontweight='bold', y=1.01
)
plt.savefig(save_path, dpi=150, bbox_inches='tight', facecolor=BG)
print(f"\n[PLOT] Saved → {save_path}")
# Also save JSON
json_path = save_path.replace(".png", ".json")
with open(json_path, "w") as f:
json.dump({
"summary": {
"episodes": len(results),
"accuracy": accuracy,
"avg_reward": float(np.mean(rewards)),
"total_correct": int(sum(correct)),
},
"episodes": results,
}, f, indent=2)
print(f"[DATA] Saved → {json_path}")
plt.show()
# ── Main ──────────────────────────────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--episodes", type=int, default=20)
parser.add_argument("--model", default=None, help="HF model repo for decisions")
parser.add_argument("--env-url", default=ENV_URL)
parser.add_argument("--output", default="cace_inference_results.png")
args = parser.parse_args()
env = CACEClient(args.env_url)
env.wait_until_ready()
info = env.info()
print(f"\n[START] env={info.get('name','cace')} model={args.model or 'rule-based'}")
print(f" action_space={info.get('action_space',{}).get('n')} reward_range={info.get('reward_range')}\n")
results = run_episodes(env, args.episodes, args.model)
# Summary
rewards = [r["reward"] for r in results]
accuracy = sum(r["correct"] for r in results) / len(results)
print(f"\n[END] episodes={len(results)} accuracy={accuracy:.3f} "
f"avg_reward={np.mean(rewards):.3f} "
f"rewards={','.join(f'{r:.2f}' for r in rewards)}")
metrics = env.metrics()
print(f"[METRICS] {metrics}")
plot_results(results, args.output)
if __name__ == "__main__":
main()