RevLogiXenv / inference.py
AdityaProCoder
docs+inference: deepen READMEs and align END log format
01961e6
Raw History Blame Contribute Delete
5.82 kB
"""
Hackathon-compliant inference script for RevLogiXenv.
Outputs structured stdout logs for Meta OpenEnv Hackathon Phase 2 validation.
Required Environment Variables:
- API_BASE_URL: OpenAI-compatible endpoint.
- MODEL_NAME: LLM identifier (e.g., gpt-4o-mini).
- HF_TOKEN: API key for the LLM.
- TASKS: Comma-separated list of tasks (easy,medium,hard).
Format:
[START] task=<task> env=<benchmark> model=<model>
[STEP] step=<n> action=<action> reward=<0.00> done=<true|false> error=<msg|null>
[END] success=<true|false> steps=<n> score=<0.00> rewards=<r1,r2,...>
"""
from __future__ import annotations
import os
import re
from dotenv import load_dotenv
from openai import OpenAI
from RevLogiXenv_v0 import AutonomousReturnsEnv
from RevLogiXenv_v0.models import DispositionAction, ReturnsAction, ReturnsObservation
from RevLogiXenv_v0.prompting import (
MINIMAL_SYSTEM_PROMPT,
observation_to_prompt_inference as observation_to_prompt,
parse_action_inference as parse_action,
)
load_dotenv()
API_BASE_URL = os.getenv("API_BASE_URL", "https://api.openai.com/v1")
MODEL_NAME = os.getenv("MODEL_NAME", "gpt-4.1-mini")
HF_TOKEN = os.getenv("HF_TOKEN")
if not HF_TOKEN:
raise ValueError("HF_TOKEN environment variable is required")
TASKS = os.getenv("TASKS", "easy,medium,hard")
BENCHMARK = os.getenv("BENCHMARK", "revlogixenv_v0")
MAX_STEPS = 100
SUCCESS_SCORE_THRESHOLD = 0.5
TEMPERATURE = 0.15
MAX_TOKENS = 64
# Bounds that keep every per-step reward strictly within (0, 1).
# The hackathon validator derives the task score from the rewards list;
# keeping every value in (0.01, 0.99) guarantees any aggregation
# (average, sum-normalised, etc.) stays within the required open interval.
_REWARD_MIN = 0.01
_REWARD_MAX = 0.99
def log_start(task: str, env: str, model: str) -> None:
print(f"[START] task={task} env={env} model={model}", flush=True)
def log_step(step: int, action: str, reward: float, done: bool, error: str | None) -> None:
# Spec: done is lowercase boolean string
done_str = "true" if done else "false"
error_value = error if error else "null"
print(
f"[STEP] step={step} action={action} reward={reward:.2f} "
f"done={done_str} error={error_value}",
flush=True,
)
def log_end(success: bool, steps: int, score: float, rewards: list[float]) -> None:
"""Final task log — matches reference repo format exactly."""
rewards_str = ",".join(f"{r:.2f}" for r in rewards)
print(f"[END] success={str(success).lower()} steps={steps} score={score:.2f} rewards={rewards_str}", flush=True)
def _clamp_reward(r: float) -> float:
"""Ensure every per-step reward is strictly within (0, 1)."""
return max(_REWARD_MIN, min(_REWARD_MAX, r))
def get_model_response(client: OpenAI, prompt: str) -> str:
try:
completion = client.chat.completions.create(
model=MODEL_NAME,
messages=[
{"role": "system", "content": MINIMAL_SYSTEM_PROMPT},
{"role": "user", "content": prompt},
],
temperature=TEMPERATURE,
max_tokens=MAX_TOKENS + 1024,
stream=False,
extra_body={"reasoning_split": True},
)
content = (completion.choices[0].message.content or "").strip()
if content:
return content
reasoning_details = getattr(completion.choices[0].message, "reasoning_details", None)
if isinstance(reasoning_details, list):
parts: list[str] = []
for chunk in reasoning_details:
if isinstance(chunk, dict) and "text" in chunk:
parts.append(chunk["text"] or "")
return "".join(parts)
except Exception:
return "wait"
return "wait"
def run_episode(client: OpenAI, task_name: str) -> tuple[bool, int, list[float]]:
rewards: list[float] = []
steps_taken = 0
success = False
log_start(task=task_name, env=BENCHMARK, model=MODEL_NAME)
env = AutonomousReturnsEnv(task_name)
try:
obs = env.reset(seed=42)
for step in range(1, MAX_STEPS + 1):
if obs.done:
break
prompt = observation_to_prompt(obs)
message = get_model_response(client, prompt)
action = parse_action(message, obs)
obs = env.step(action)
# Clamp to (0.01, 0.99) so the rewards CSV never contains 0.0 or 1.0,
# keeping the validator's derived task score strictly within (0, 1).
raw_reward = float(obs.reward) if obs.reward is not None else _REWARD_MIN
reward = _clamp_reward(raw_reward)
done = bool(obs.done)
error = None
if isinstance(obs.metadata, dict):
raw_error = obs.metadata.get("last_action_error")
error = raw_error if isinstance(raw_error, str) and raw_error else None
rewards.append(reward)
steps_taken = step
log_step(step=step, action=action.action.value, reward=reward, done=done, error=error)
if done:
break
score = sum(rewards) / len(rewards) if rewards else 0.0
score = max(1e-6, min(score, 1 - 1e-6)) # strictly within (0, 1) — matches reference
success = score >= SUCCESS_SCORE_THRESHOLD
finally:
try:
env.close()
except Exception:
pass
log_end(success=success, steps=steps_taken, score=score, rewards=rewards)
return success, steps_taken, rewards
def main() -> None:
client = OpenAI(base_url=API_BASE_URL, api_key=HF_TOKEN)
task_names = [t.strip() for t in TASKS.split(",") if t.strip()]
for task_name in task_names:
run_episode(client, task_name)
if __name__ == "__main__":
main()