Spaces:
Sleeping
Sleeping
File size: 10,100 Bytes
753b5ee 6026955 753b5ee 6026955 753b5ee 6026955 1816098 6026955 753b5ee 6026955 753b5ee caa07e1 753b5ee f15c8b1 753b5ee f15c8b1 753b5ee f15c8b1 753b5ee f15c8b1 753b5ee f15c8b1 753b5ee f15c8b1 753b5ee f15c8b1 753b5ee f15c8b1 753b5ee f15c8b1 82f2af5 f15c8b1 753b5ee | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 | """
Inference Script β Email Triage RL Environment
===================================
MANDATORY environment variables (injected by hackathon validator):
API_BASE_URL The LiteLLM proxy endpoint (REQUIRED)
API_KEY The proxy API key (REQUIRED)
MODEL_NAME The model identifier (default: gpt-4o-mini)
TASK_LEVEL easy | medium | hard (default: easy)
STDOUT FORMAT (required by hackathon evaluator):
[START] task=<task_name> env=<benchmark> model=<model_name>
[STEP] step=<n> action=<action_str> reward=<0.00> done=<true|false> error=<msg|null>
[END] success=<true|false> steps=<n> score=<score> rewards=<r1,r2,...,rn>
Usage (local testing only):
# Against local server:
API_KEY=hf_xxx API_BASE_URL=https://router.huggingface.co/v1 python inference.py
"""
import asyncio
import json
import os
import textwrap
from typing import List, Optional
from openai import OpenAI
from email_triage_rl_hackathon import EmailTriageEnv, TriageAction
# ββ Config ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# These are injected by the hackathon validator. Read them at call time inside main()
# so they are resolved AFTER the validator sets them β never use a module-level default
# that could silently bypass the proxy.
MODEL_NAME = os.environ.get("MODEL_NAME", "gpt-4o-mini")
# ENV_BASE_URL: validator injects this pointing to the running environment server.
# Fallback to the HF Space URL for local testing without ENV_BASE_URL set.
ENV_BASE_URL = os.environ.get("ENV_BASE_URL", "https://Shivacode-rl-hackathon.hf.space")
LOCAL_IMAGE_NAME = os.environ.get("LOCAL_IMAGE_NAME", "")
TASK_LEVEL = os.environ.get("TASK_LEVEL", "easy") # easy | medium | hard
BENCHMARK = "email_triage_rl_hackathon"
MAX_STEPS = 15
TEMPERATURE = 0.0
MAX_TOKENS = 512
SUCCESS_SCORE_THRESHOLD = 0.5
# ββ Prompts βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
SYSTEM_PROMPT = textwrap.dedent("""
You are an expert email triage agent working for a company.
For each email you receive, respond with a JSON object with EXACTLY these fields:
{
"category": one of ["spam", "billing", "technical", "hr", "general"],
"priority": one of ["low", "medium", "high", "urgent"],
"department": one of ["support", "finance", "engineering", "hr", "management"],
"reply": "your professional reply draft (empty string for easy tasks)"
}
Routing rules:
- spam β category=spam, priority=low, department=support
- payment/invoice β category=billing, priority=high, department=finance
- bugs/login/API β category=technical, priority=urgent, department=engineering
- leave/HR/complaint β category=hr, priority=urgent, department=hr
- board/executive β category=hr, priority=urgent, department=management
- general/supplies β category=general, priority=low, department=support
For medium/hard tasks: write a professional reply (minimum 2 sentences).
For urgent issues: always acknowledge urgency and give a timeframe in the reply.
Respond ONLY with the JSON object, no other text.
""").strip()
# ββ Logging helpers (required stdout format) ββββββββββββββββββββββββββββββββββ
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: Optional[str]) -> None:
error_val = error if error else "null"
done_val = str(done).lower()
print(
f"[STEP] step={step} action={action} reward={reward:.2f} done={done_val} error={error_val}",
flush=True,
)
def log_end(success: bool, steps: int, score: float, rewards: List[float]) -> None:
rewards_str = ",".join(f"{r:.2f}" for r in rewards)
print(
f"[END] success={str(success).lower()} steps={steps} score={score:.3f} rewards={rewards_str}",
flush=True,
)
# ββ LLM call βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def build_user_prompt(obs, step: int, history: List[str]) -> str:
history_block = "\n".join(history[-3:]) if history else "None"
return textwrap.dedent(f"""
Step: {step}
Task level: {obs.task_level}
Emails remaining after this: {obs.emails_remaining}
--- EMAIL ---
From: {obs.sender}
Subject: {obs.subject}
{obs.body}
--- END EMAIL ---
Previous steps:
{history_block}
{"Note: Write a professional reply (reply field required)." if obs.task_level in ("medium", "hard") else "Note: Reply field can be empty string for easy tasks."}
""").strip()
def get_model_action(client: OpenAI, obs, step: int, history: List[str]) -> TriageAction:
"""Call the LLM and parse the JSON response into a TriageAction."""
user_prompt = build_user_prompt(obs, step, history)
completion = client.chat.completions.create(
model=MODEL_NAME,
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_prompt},
],
temperature=TEMPERATURE,
max_tokens=MAX_TOKENS,
stream=False,
)
content = (completion.choices[0].message.content or "").strip()
# Strip markdown code fences if present
if content.startswith("```"):
content = content.split("```")[1]
if content.startswith("json"):
content = content[4:]
content = content.strip()
data = json.loads(content)
return TriageAction(**data)
# ββ Main episode loop βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
async def run_episode(
client: OpenAI,
env: EmailTriageEnv,
task_level: str,
) -> None:
"""Run one full episode for a given task level, emitting START/STEP/END logs."""
history: List[str] = []
rewards: List[float] = []
steps_taken: int = 0
score: float = 0.01
success: bool = False
log_start(task=task_level, env=BENCHMARK, model=MODEL_NAME)
try:
reset_result = await env.reset(seed=42, task_level=task_level)
obs = reset_result.observation if hasattr(reset_result, "observation") else reset_result
for step in range(1, MAX_STEPS + 1):
if obs.done or obs.email_id == "none":
break
action = get_model_action(client, obs, step, history)
action_str = (
f"category={action.category},priority={action.priority},"
f"department={action.department},"
f"reply={repr(action.reply[:40]) if action.reply else repr('')}"
)
result = await env.step(action)
reward = result.reward or 0.01
done = result.done
steps_taken = step
obs = result.observation
rewards.append(reward)
log_step(step=step, action=action_str, reward=reward, done=done, error=None)
history.append(
f"Step {step} [{obs.email_id}]: "
f"cat={action.category} pri={action.priority} "
f"β reward={reward:.2f} | {obs.feedback[:60]}"
)
if done:
break
score = sum(rewards) / len(rewards) if rewards else 0.01
score = min(max(score, 0.01), 0.99) # strictly (0, 1) as required
success = score >= SUCCESS_SCORE_THRESHOLD
except Exception as exc:
print(f"[DEBUG] Episode error ({task_level}): {exc}", flush=True)
score = 0.01
log_end(success=success, steps=steps_taken, score=score, rewards=rewards)
async def main() -> None:
api_key = os.environ.get("API_KEY") or os.environ.get("HF_TOKEN") or os.environ.get("OPENAI_API_KEY")
api_base_url = os.environ.get("API_BASE_URL")
if not api_key:
raise EnvironmentError(
"API_KEY is not set. The hackathon validator injects API_KEY automatically.\n"
"For local testing: export API_KEY=hf_your_token_here"
)
if not api_base_url:
api_base_url = "https://router.huggingface.co/v1"
print(f"[DEBUG] API_BASE_URL not set β using fallback: {api_base_url}", flush=True)
print(f"[DEBUG] API_BASE_URL={api_base_url}", flush=True)
print(f"[DEBUG] MODEL_NAME={MODEL_NAME}", flush=True)
client = OpenAI(api_key=api_key, base_url=api_base_url)
# Always run all three task levels β the validator requires at least 3 graded tasks.
# TASK_LEVEL env var is intentionally ignored here; it only affects the uvicorn server.
task_levels = ["easy", "medium", "hard"]
print(f"[DEBUG] Running task levels: {task_levels}", flush=True)
if LOCAL_IMAGE_NAME:
print(f"[DEBUG] Connecting via Docker image: {LOCAL_IMAGE_NAME}", flush=True)
env = await EmailTriageEnv.from_docker_image(LOCAL_IMAGE_NAME)
else:
print(f"[DEBUG] Connecting to server: {ENV_BASE_URL}", flush=True)
env = EmailTriageEnv(base_url=ENV_BASE_URL)
try:
for task_level in task_levels:
await run_episode(client, env, task_level)
finally:
try:
await env.close()
except Exception as e:
print(f"[DEBUG] env.close() error: {e}", flush=True)
if __name__ == "__main__":
asyncio.run(main())
|