| """ |
| Inference Script β AI Sprint Manager OpenEnv |
| ============================================================ |
| MANDATORY: |
| API_BASE_URL : LLM endpoint |
| MODEL_NAME : Model identifier |
| HF_TOKEN : Hugging Face / API key |
| """ |
| from __future__ import annotations |
| import os |
| import json |
| import time |
| import sys |
| import requests |
| from dotenv import load_dotenv |
| from openai import OpenAI |
|
|
| load_dotenv() |
|
|
| |
| API_BASE_URL = os.getenv("API_BASE_URL", "https://router.huggingface.co/v1") |
| API_KEY = os.getenv("HF_TOKEN") or os.getenv("API_KEY", "dummy") |
| MODEL_NAME = os.getenv("MODEL_NAME", "meta-llama/Llama-3.1-8B-Instruct") |
| ENV_BASE_URL = os.getenv("ENV_BASE_URL", "https://sejal-k-ai-sprint-manager.hf.space") |
|
|
| MAX_STEPS = 12 |
| TEMPERATURE = 0.2 |
| MAX_TOKENS = 300 |
| TASKS = ["easy_sprint", "medium_sprint", "hard_sprint"] |
|
|
| client = OpenAI(base_url=API_BASE_URL, api_key=API_KEY) |
|
|
| SYSTEM_PROMPT = """You are an expert Tech Lead managing an agile sprint. |
| Your goal: maximize task completion, balance developer workload, and meet deadlines. |
| |
| Each step output a JSON action with this exact schema: |
| { |
| "action_type": "<assign|reassign|reprioritize|unblock|skip>", |
| "task_id": "<task id or null>", |
| "dev_id": "<developer id or null>", |
| "new_priority": <1-5 or null> |
| } |
| |
| Rules: |
| - assign: put a backlog task onto an available developer |
| - reassign: move an in-progress task to a different developer |
| - reprioritize: change a task priority (1=highest) |
| - unblock: unblock a blocked task |
| - skip: do nothing |
| |
| Output ONLY the JSON object. No explanation.""" |
|
|
|
|
| def build_user_prompt(obs: dict) -> str: |
| tasks_summary = "\n".join( |
| f" [{t['id']}] {t['name']} | {t['task_type']} | P{t['priority']} | " |
| f"effort={t['effort']} | due=Day{t['deadline']} | status={t['status']} | " |
| f"dev={t['assigned_to']} | progress={t['progress']:.0%}" |
| for t in obs["tasks"] |
| ) |
| devs_summary = "\n".join( |
| f" [{d['id']}] {d['name']} | skill={d['skill']} | " |
| f"load={d['current_load']}/{d['capacity']} | available={d['is_available']}" |
| for d in obs["developers"] |
| ) |
| events_str = "\n ".join(obs.get("events", [])) or "None" |
| return f"""Day: {obs['current_day']}/{obs['sprint_length']} |
| Done:{obs['tasks_completed']} Missed:{obs['tasks_missed']} InProgress:{obs['tasks_in_progress']} Backlog:{obs['tasks_backlog']} |
| Cumulative Reward: {obs['cumulative_reward']:.2f} |
| |
| Events: {events_str} |
| |
| TASKS: |
| {tasks_summary} |
| |
| DEVELOPERS: |
| {devs_summary} |
| |
| Output your JSON action:""" |
|
|
|
|
| def call_env(endpoint: str, payload: dict = None, method: str = "POST") -> dict: |
| url = f"{ENV_BASE_URL}/{endpoint}" |
| if method == "GET": |
| resp = requests.get(url, timeout=30) |
| else: |
| resp = requests.post(url, json=payload or {}, timeout=30) |
| resp.raise_for_status() |
| return resp.json() |
|
|
|
|
| def get_rule_based_action(obs: dict) -> str: |
| """Fallback rule-based action when LLM unavailable.""" |
| tasks = obs.get("tasks", []) |
| devs = obs.get("developers", []) |
| backlog = sorted( |
| [t for t in tasks if t["status"] == "backlog"], |
| key=lambda t: (t["priority"], t["deadline"]) |
| ) |
| if not backlog: |
| return '{"action_type": "skip"}' |
| task = backlog[0] |
| available = [d for d in devs if d["is_available"] and d["current_load"] < d["capacity"]] |
| skill_match = [d for d in available if d["skill"] == task["required_skill"] or d["skill"] == "fullstack"] |
| dev = skill_match[0] if skill_match else (available[0] if available else None) |
| if not dev: |
| return '{"action_type": "skip"}' |
| return json.dumps({"action_type": "assign", "task_id": task["id"], "dev_id": dev["id"], "new_priority": None}) |
|
|
|
|
| def parse_action(text: str) -> dict: |
| text = text.strip() |
| if "```" in text: |
| lines = [l for l in text.split("\n") if not l.strip().startswith("```")] |
| text = "\n".join(lines) |
| try: |
| return json.loads(text) |
| except json.JSONDecodeError: |
| start, end = text.find("{"), text.rfind("}") + 1 |
| if start >= 0 and end > start: |
| try: |
| return json.loads(text[start:end]) |
| except Exception: |
| pass |
| return {"action_type": "skip", "task_id": None, "dev_id": None, "new_priority": None} |
|
|
|
|
| def run_episode(task_name: str) -> float: |
| """Run one complete episode and return final score.""" |
|
|
| |
| print(f"[START] task={task_name}", flush=True) |
|
|
| obs = call_env("reset", {"task_name": task_name, "seed": 42}) |
| final_score = 0.0 |
| step_num = 0 |
|
|
| for step_num in range(1, MAX_STEPS + 1): |
| if obs.get("done", False): |
| break |
|
|
| try: |
| completion = client.chat.completions.create( |
| model=MODEL_NAME, |
| messages=[ |
| {"role": "system", "content": SYSTEM_PROMPT}, |
| {"role": "user", "content": build_user_prompt(obs)}, |
| ], |
| temperature=TEMPERATURE, |
| max_tokens=MAX_TOKENS, |
| ) |
| response_text = completion.choices[0].message.content or "" |
| except Exception as e: |
| response_text = get_rule_based_action(obs) |
|
|
| action = parse_action(response_text) |
| result = call_env("step", {"action": action}) |
| obs = result["observation"] |
| reward = result["reward"] |
| done = result["done"] |
| info = result.get("info", {}) |
|
|
| |
| print( |
| f"[STEP] task={task_name} step={step_num} " |
| f"action={action.get('action_type')} reward={reward:.4f} " |
| f"cumulative={obs.get('cumulative_reward', 0):.4f} done={done}", |
| flush=True |
| ) |
|
|
| if done: |
| final_score = max(0.01, min(0.99, info.get("final_score", 0.01))) |
| break |
|
|
| |
| print( |
| f"[END] task={task_name} score={final_score:.4f} steps={step_num}", |
| flush=True |
| ) |
| return final_score |
|
|
|
|
| def main(): |
| print(f"[INFO] model={MODEL_NAME} server={ENV_BASE_URL}", flush=True) |
|
|
| try: |
| health = call_env("health", method="GET") |
| print(f"[INFO] health={health}", flush=True) |
| except Exception as e: |
| print(f"[ERROR] Cannot reach env server: {e}", flush=True) |
| sys.exit(1) |
|
|
| scores = {} |
| start_time = time.time() |
|
|
| for task in TASKS: |
| try: |
| score = run_episode(task) |
| scores[task] = score |
| except Exception as e: |
| print(f"[ERROR] task={task} error={e}", flush=True) |
| scores[task] = 0.0 |
|
|
| elapsed = time.time() - start_time |
|
|
| |
| print("\n" + "="*60, flush=True) |
| print(" BASELINE SCORES", flush=True) |
| print("="*60, flush=True) |
| for task, score in scores.items(): |
| bar = "β" * int(score * 20) |
| print(f" {task:<20} {score:.4f} {bar}", flush=True) |
| avg = sum(scores.values()) / len(scores) if scores else 0.0 |
| print(f" {'AVERAGE':<20} {avg:.4f}", flush=True) |
| print(f"\n Runtime: {elapsed:.1f}s", flush=True) |
| print("="*60, flush=True) |
|
|
|
|
| if __name__ == "__main__": |
| main() |