Download server.py from SmritiS21/AryaX: direct link, hf CLI and curl.
- Browser
- Download file 24.1 kB
-
https://huggingface.co/spaces/SmritiS21/AryaX/resolve/main/server.py
- Command line
-
hf download hf://spaces/SmritiS21/AryaX/server.py
-
curl -L -o server.py https://huggingface.co/spaces/SmritiS21/AryaX/resolve/main/server.py
24.1 kB
| """ | |
| SentinelEnv / Arya-X server — Flask API + dashboard UI. | |
| Single-agent endpoints preserved; multi-agent endpoints added under /reset_multi, | |
| /step_multi, /auto_multi. | |
| Run: python server.py | |
| """ | |
| import os | |
| import json | |
| import random as _random | |
| from pathlib import Path | |
| from flask import Flask, request, jsonify, render_template | |
| from env import SentinelEnv | |
| from env.models import Action | |
| from env.multiagent import AryaXEnv, Proposal, AGENT_TYPES | |
| from agents.satellite import SatelliteAgent | |
| from agents.drone import DroneAgent | |
| from agents.radar import RadarAgent | |
| from agents.command import CommandAgent | |
| app = Flask(__name__) | |
| # ── Single-agent env (existing) ─────────────────────────────────────────────── | |
| env = SentinelEnv(max_steps=10, seed=42) | |
| obs = None | |
| _target_positions: dict = {} | |
| # ── Multi-agent env ─────────────────────────────────────────────────────────── | |
| mx_env = AryaXEnv(max_steps=10, seed=42, mode='single') | |
| mx_obs = None # Dict[str, AgentObservation] | None | |
| sat_agent = SatelliteAgent() | |
| drone_agent = DroneAgent() | |
| radar_agent = RadarAgent() | |
| command_agent = CommandAgent() | |
| _llm_client = None | |
| API_BASE_URL = os.environ.get("API_BASE_URL", "https://router.huggingface.co/v1") | |
| MODEL_NAME = os.environ.get("MODEL_NAME", "meta-llama/Llama-3.2-3B-Instruct") | |
| HF_TOKEN = os.environ.get("HF_TOKEN") | |
| _base_model = None | |
| _tokenizer = None | |
| _has_adapters = False | |
| # Fallback mappings for local checkpoints | |
| AGENT_ID_MAP = {"satellite": "SAT", "drone": "UAV", "radar": "RDR", "command": "CMD"} | |
| try: | |
| import torch | |
| from transformers import AutoTokenizer, AutoModelForCausalLM | |
| from peft import PeftModel | |
| _LOCAL_HF_AVAILABLE = True | |
| except ImportError: | |
| _LOCAL_HF_AVAILABLE = False | |
| def init_local_models(): | |
| global _base_model, _tokenizer, _has_adapters | |
| if not _LOCAL_HF_AVAILABLE: | |
| print("[WARN] transformers or peft not installed. Will use greedy fallback.") | |
| return | |
| checkpoint_dir = Path("./checkpoints/arya_x_lora") | |
| adapter_file = checkpoint_dir / "adapter_model.safetensors" | |
| if not adapter_file.exists(): | |
| print(f"[WARN] No adapter found at {checkpoint_dir}. Will use greedy fallback.") | |
| return | |
| adapter_config = checkpoint_dir / "adapter_config.json" | |
| base_model_name = MODEL_NAME | |
| try: | |
| with open(adapter_config) as f: | |
| cfg = json.load(f) | |
| base_model_name = cfg.get("base_model_name_or_path", MODEL_NAME) | |
| except Exception: | |
| pass | |
| print(f"[LLM] Loading base model ({base_model_name}) + LoRA adapter...") | |
| try: | |
| _tokenizer = AutoTokenizer.from_pretrained(str(checkpoint_dir), padding_side="left") | |
| if not torch.cuda.is_available(): | |
| print("[WARN] No GPU detected — skipping local adapter load. Using remote API.") | |
| return | |
| load_kwargs = {"load_in_4bit": True, "device_map": "auto"} | |
| base = AutoModelForCausalLM.from_pretrained(base_model_name, **load_kwargs) | |
| _base_model = PeftModel.from_pretrained(base, str(checkpoint_dir)) | |
| _has_adapters = True | |
| print("[LLM] LoRA adapter loaded successfully.") | |
| except Exception as e: | |
| print(f"[ERROR] Failed to load adapter: {e}") | |
| _has_adapters = False | |
| # Initialize — try local adapter first, fall back to remote API | |
| import sys | |
| print(f"[ENV] HF_TOKEN set: {bool(HF_TOKEN)}, MODEL_NAME: {MODEL_NAME}", flush=True, file=sys.stderr) | |
| init_local_models() | |
| if not _has_adapters: | |
| if HF_TOKEN: | |
| try: | |
| from openai import OpenAI | |
| _llm_client = OpenAI(base_url=API_BASE_URL, api_key=HF_TOKEN) | |
| print(f"[LLM] Connected to remote API: {MODEL_NAME}", flush=True, file=sys.stderr) | |
| except Exception as e: | |
| print(f"[LLM] Failed to init client: {e}. Using greedy fallback.", flush=True, file=sys.stderr) | |
| else: | |
| print("[LLM] No HF_TOKEN set — using greedy fallback.", flush=True, file=sys.stderr) | |
| # ── Single-agent helpers (unchanged) ───────────────────────────────────────── | |
| def _build_prompt(observation) -> str: | |
| sensors = "\n".join( | |
| f" - id={s.id} type={s.type} range={s.range}km available={s.available}" | |
| for s in observation.sensors if s.available | |
| ) | |
| targets = "\n".join( | |
| f" - id={t.id} priority={t.priority} active={t.active}" | |
| for t in observation.targets if t.active | |
| ) | |
| n = sum(1 for s in observation.sensors if s.available) | |
| return f"""You are a military sensor allocation AI. Assign ALL available sensors to threats. | |
| Priority 3=HIGH (missile/critical), 2=MED (border movement), 1=LOW (airspace). | |
| Always cover HIGH priority threats first. Each sensor must go to a DIFFERENT target. | |
| Timestep: {observation.timestep} | |
| Available Sensors ({n}): | |
| {sensors} | |
| Active Threats: | |
| {targets} | |
| Respond ONLY with a JSON array of assignments, one per available sensor: | |
| [{{"sensor_id": "S1", "target_id": "T0_1"}}, {{"sensor_id": "S2", "target_id": "T0_2"}}] | |
| """ | |
| def _parse_llm_actions(text: str, observation) -> list[Action]: | |
| try: | |
| start = text.find("[") | |
| end = text.rfind("]") + 1 | |
| data = json.loads(text[start:end]) | |
| valid_sensors = {s.id for s in observation.sensors if s.available} | |
| valid_targets = {t.id for t in observation.targets if t.active} | |
| actions, used_sensors, used_targets = [], set(), set() | |
| for item in data: | |
| sid, tid = item.get("sensor_id"), item.get("target_id") | |
| if (sid in valid_sensors and tid in valid_targets | |
| and sid not in used_sensors and tid not in used_targets): | |
| actions.append(Action(sensor_id=sid, target_id=tid)) | |
| used_sensors.add(sid) | |
| used_targets.add(tid) | |
| return actions | |
| except Exception: | |
| return [] | |
| def _greedy_actions(observation) -> list[Action]: | |
| available = [s for s in observation.sensors if s.available] | |
| targets = sorted([t for t in observation.targets if t.active], key=lambda t: -t.priority) | |
| actions, used = [], set() | |
| for sensor in available: | |
| for target in targets: | |
| if target.id not in used: | |
| actions.append(Action(sensor_id=sensor.id, target_id=target.id)) | |
| used.add(target.id) | |
| break | |
| return actions | |
| def _get_actions(observation) -> tuple[list[Action], str]: | |
| if _llm_client and not _has_adapters: # Only use remote if local is missing | |
| try: | |
| prompt = _build_prompt(observation) | |
| response = _llm_client.chat.completions.create( | |
| model=MODEL_NAME, | |
| messages=[{"role": "user", "content": prompt}], | |
| max_tokens=256, | |
| temperature=0.0 | |
| ) | |
| raw = response.choices[0].message.content.strip() | |
| actions = _parse_llm_actions(raw, observation) | |
| if actions: | |
| return actions, "llm" | |
| print(f"[LLM] Bad response, falling back. Raw: {raw!r}") | |
| except Exception as e: | |
| print(f"[LLM] Error: {e}. Falling back to greedy.") | |
| return _greedy_actions(observation), "greedy" | |
| # ── Multi-agent helpers ─────────────────────────────────────────────────────── | |
| def _build_multi_prompt(agent_id: str, agent_obs, used_sensors: set = None) -> str: | |
| used_sensors = used_sensors or set() | |
| my_sensors = [ | |
| s for s in agent_obs.sensors | |
| if s["available"] and s["id"] not in used_sensors | |
| and (agent_id == "command" or s["type"] == agent_id) | |
| ] | |
| sensors = "\n".join( | |
| f" - id={s['id']} type={s['type']} range={s['range']}km" | |
| for s in my_sensors | |
| ) | |
| targets = "\n".join( | |
| f" - id={t['id']} priority={t['priority']}" | |
| for t in agent_obs.targets if t["active"] | |
| ) | |
| if not my_sensors: | |
| return "" | |
| return f"""You are the {agent_id} agent in a multi-agent ISR system. | |
| You may ONLY assign YOUR sensors listed below. Do NOT use sensors belonging to other agents. | |
| Priority 3=HIGH, 2=MED, 1=LOW. Cover HIGH threats first. | |
| Timestep: {agent_obs.timestep} | |
| Your sensors ({agent_id} type only): | |
| {sensors} | |
| Active Threats: | |
| {targets} | |
| Respond ONLY with a JSON array using only your sensors above: | |
| [{{"sensor_id": "S1", "target_id": "T0_1"}}] | |
| """ | |
| def _greedy_proposals(agent_id: str, agent_obs, used_sensors: set, used_targets: set = None) -> list[Proposal]: | |
| """Greedy proposals for one agent — only claim sensors matching agent type.""" | |
| if used_targets is None: | |
| used_targets = set() | |
| my_sensors = [ | |
| s for s in agent_obs.sensors | |
| if s["available"] and s["id"] not in used_sensors | |
| and (agent_id == "command" or s["type"] == agent_id) | |
| ] | |
| targets = sorted( | |
| [t for t in agent_obs.targets if t["active"]], | |
| key=lambda t: -t["priority"] | |
| ) | |
| proposals = [] | |
| for sensor in my_sensors: | |
| for target in targets: | |
| if target["id"] not in used_targets: | |
| proposals.append(Proposal( | |
| agent_id=agent_id, | |
| sensor_id=sensor["id"], | |
| target_id=target["id"] | |
| )) | |
| used_targets.add(target["id"]) | |
| break | |
| return proposals | |
| def _lora_multi_proposals(agent_obs_map) -> tuple[list[Proposal], str]: | |
| """Build multi-agent proposals using single shared LoRA adapter.""" | |
| proposals: list[Proposal] = [] | |
| used_sensors: set = set() | |
| used_targets: set = set() | |
| for agent_id in AGENT_TYPES: | |
| agent_obs = agent_obs_map[agent_id] | |
| prompt = _build_multi_prompt(agent_id, agent_obs) | |
| chat_prompt = f"<|begin_of_text|><|start_header_id|>user<|end_header_id|>\n\n{prompt}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n" | |
| try: | |
| inputs = _tokenizer(chat_prompt, return_tensors="pt").to(_base_model.device) | |
| outputs = _base_model.generate(**inputs, max_new_tokens=128, temperature=0.1, do_sample=True, pad_token_id=_tokenizer.eos_token_id) | |
| raw = _tokenizer.decode(outputs[0], skip_special_tokens=True) | |
| if "assistant" in raw: | |
| raw = raw.split("assistant")[-1].strip() | |
| start, end = raw.find("["), raw.rfind("]") + 1 | |
| if start == -1 or end == 0: | |
| raise ValueError("no JSON array") | |
| items = json.loads(raw[start:end]) | |
| valid_sensors = {s["id"] for s in agent_obs.sensors if s["available"] and s["id"] not in used_sensors} | |
| valid_targets = {t["id"] for t in agent_obs.targets if t["active"] and t["id"] not in used_targets} | |
| for item in items: | |
| sid, tid = item.get("sensor_id"), item.get("target_id") | |
| if sid in valid_sensors and tid in valid_targets: | |
| proposals.append(Proposal(agent_id=agent_id, sensor_id=sid, target_id=tid)) | |
| used_sensors.add(sid) | |
| used_targets.add(tid) | |
| break | |
| except Exception as e: | |
| print(f"[WARN] LoRA generation failed for {agent_id}: {e} — using greedy") | |
| cmd_obs = agent_obs_map["command"] | |
| for p in _greedy_proposals(agent_id, cmd_obs, used_sensors, set(used_targets)): | |
| proposals.append(p) | |
| used_sensors.add(p.sensor_id) | |
| used_targets.add(p.target_id) | |
| break | |
| return proposals, "lora" | |
| def _get_multi_proposals(agent_obs_map) -> tuple[list[Proposal], str]: | |
| """Build proposals from all agents. Returns (proposals, source).""" | |
| proposals: list[Proposal] = [] | |
| used_sensors: set = set() | |
| if _has_adapters: | |
| return _lora_multi_proposals(agent_obs_map) | |
| if _llm_client and not _has_adapters: # API fallback | |
| try: | |
| for agent_id in AGENT_TYPES: | |
| agent_obs = agent_obs_map[agent_id] | |
| prompt = _build_multi_prompt(agent_id, agent_obs, used_sensors) | |
| if not prompt: | |
| continue | |
| response = _llm_client.chat.completions.create( | |
| model=MODEL_NAME, | |
| messages=[{"role": "user", "content": prompt}], | |
| max_tokens=64, | |
| temperature=0.0, | |
| timeout=10 | |
| ) | |
| raw = response.choices[0].message.content.strip() | |
| start, end = raw.find("["), raw.rfind("]") + 1 | |
| items = json.loads(raw[start:end]) | |
| my_sensors = {s["id"] for s in agent_obs.sensors if s["available"] and s["id"] not in used_sensors and (agent_id == "command" or s["type"] == agent_id)} | |
| valid_targets = {t["id"] for t in agent_obs.targets if t["active"]} | |
| for item in items: | |
| sid, tid = item.get("sensor_id"), item.get("target_id") | |
| if sid in my_sensors and tid in valid_targets: | |
| proposals.append(Proposal(agent_id=agent_id, sensor_id=sid, target_id=tid)) | |
| used_sensors.add(sid) | |
| if proposals: | |
| return proposals, "llm" | |
| except Exception as e: | |
| print(f"[LLM multi] Error: {e}. Falling back to greedy.", flush=True, file=sys.stderr) | |
| # Greedy fallback — shared used_sensors + used_targets prevents duplicates | |
| used_sensors = set() | |
| used_targets: set = set() | |
| cmd_obs = agent_obs_map["command"] | |
| for agent_id in AGENT_TYPES: | |
| for p in _greedy_proposals(agent_id, cmd_obs, used_sensors, used_targets): | |
| proposals.append(p) | |
| used_sensors.add(p.sensor_id) | |
| used_targets.add(p.target_id) | |
| return proposals, "greedy" | |
| # ── Single-agent routes (unchanged) ────────────────────────────────────────── | |
| def status(): | |
| return jsonify({ | |
| "status": "ok", | |
| "obs_ready": obs is not None, | |
| "mx_obs_ready": mx_obs is not None, | |
| "llm_enabled": _llm_client is not None or _has_adapters, | |
| "model": MODEL_NAME if _llm_client or _has_adapters else None, | |
| "lora_active": _has_adapters | |
| }) | |
| def reset(): | |
| global obs, _target_positions | |
| body = request.get_json(silent=True) or {} | |
| seed = body.get("seed") or _random.randint(1, 99999) | |
| max_steps = body.get("max_steps", 10) | |
| env.seed = seed | |
| env.max_steps = max_steps | |
| _target_positions = {} | |
| obs = env.reset() | |
| return jsonify({**obs.model_dump(), "seed": seed}) | |
| def step(): | |
| global obs | |
| if obs is None: | |
| return jsonify({"error": "Call /reset first"}), 400 | |
| body = request.get_json(silent=True) or {} | |
| sensor_id = body.get("sensor_id") | |
| target_id = body.get("target_id") | |
| if not sensor_id or not target_id: | |
| return jsonify({"error": "Provide sensor_id and target_id"}), 400 | |
| action = Action(sensor_id=sensor_id, target_id=target_id) | |
| obs, reward, done, info = env.step(action) | |
| return jsonify({"observation": obs.model_dump(), "reward": reward, "done": done, "info": info}) | |
| def step_auto(): | |
| global obs | |
| if obs is None: | |
| return jsonify({"error": "Call /reset first"}), 400 | |
| available = [s for s in obs.sensors if s.available] | |
| active = [t for t in obs.targets if t.active] | |
| if not available or not active: | |
| obs, reward, done, info = env.step_batch([]) | |
| return jsonify({"actions": [], "action": None, "agent": "idle", | |
| "observation": obs.model_dump(), "reward": reward, "done": done, "info": info}) | |
| actions, source = _get_actions(obs) | |
| obs, total_reward, done, info = env.step_batch(actions) | |
| return jsonify({ | |
| "actions": [a.model_dump() for a in actions], | |
| "action": actions[0].model_dump() if actions else None, | |
| "agent": source, | |
| "observation": obs.model_dump(), | |
| "reward": total_reward, | |
| "done": done, | |
| "info": info | |
| }) | |
| def state(): | |
| if obs is None: | |
| return jsonify({"error": "Call /reset first"}), 400 | |
| return jsonify(obs.model_dump()) | |
| def register_custom_target(): | |
| global obs | |
| if obs is None: | |
| return jsonify({"error": "Call /reset first"}), 400 | |
| body = request.get_json(silent=True) or {} | |
| tid = body.get("id") | |
| priority = body.get("priority", 2) | |
| lat = body.get("lat") | |
| lon = body.get("lon") | |
| if not tid or lat is None or lon is None: | |
| return jsonify({"error": "Provide id, lat, lon"}), 400 | |
| from env.models import Target | |
| _target_positions[tid] = [lat, lon] | |
| env.targets.append(Target(id=tid, priority=priority, active=True)) | |
| obs = env.state() | |
| return jsonify({"ok": True, "id": tid}) | |
| def grade(): | |
| from tasks.grader import grade_episode | |
| body = request.get_json(silent=True) or {} | |
| steps = body.get("max_steps", env.max_steps) | |
| seed = body.get("seed") or _random.randint(1, 99999) | |
| g_env = SentinelEnv(max_steps=steps, seed=seed) | |
| g_obs = g_env.reset() | |
| total_reward, done = 0.0, False | |
| while not done: | |
| actions, _ = _get_actions(g_obs) | |
| g_obs, reward, done, info = g_env.step_batch(actions) | |
| total_reward += reward | |
| score = grade_episode(total_reward, info["step_count"], num_sensors=g_env.initial_sensor_count) | |
| score = max(0.01, min(0.99, score)) | |
| return jsonify({"score": score, "total_reward": total_reward, | |
| "steps": info["step_count"], "seed": seed}) | |
| # ── Multi-agent routes ──────────────────────────────────────────────────────── | |
| def reset_multi(): | |
| global mx_obs | |
| body = request.get_json(silent=True) or {} | |
| seed = body.get("seed") or _random.randint(1, 99999) | |
| max_steps = body.get("max_steps", 10) | |
| density_factor = body.get("density_factor", 1.5) | |
| failure_prob = body.get("failure_prob", 0.0) | |
| conflict_injection = body.get("conflict_injection", False) | |
| mx_env.seed = seed | |
| mx_env.max_steps = max_steps | |
| mx_env.density_factor = density_factor | |
| mx_env.failure_prob = failure_prob | |
| mx_env.conflict_injection = conflict_injection | |
| mx_obs = mx_env.reset() | |
| return jsonify({ | |
| "seed": seed, | |
| "max_steps": max_steps, | |
| "observations": {k: v.to_dict() for k, v in mx_obs.items()}, | |
| "conflict_rate": 0.0, | |
| "per_agent_rewards": {a: 0.0 for a in AGENT_TYPES}, | |
| }) | |
| def step_multi(): | |
| global mx_obs | |
| if mx_obs is None: | |
| return jsonify({"error": "Call /reset_multi first"}), 400 | |
| body = request.get_json(silent=True) or {} | |
| raw_proposals = body.get("proposals", []) | |
| proposals = [ | |
| Proposal( | |
| agent_id=p["agent_id"], | |
| sensor_id=p["sensor_id"], | |
| target_id=p["target_id"] | |
| ) | |
| for p in raw_proposals | |
| if p.get("agent_id") and p.get("sensor_id") and p.get("target_id") | |
| ] | |
| mx_obs, step_rewards, done, info = mx_env.step_multiagent(proposals) | |
| conflict_rate = info["conflict_rate"] | |
| conflicts = info["conflicts"] | |
| return jsonify({ | |
| "observations": {k: v.to_dict() for k, v in mx_obs.items()}, | |
| "step_rewards": step_rewards, | |
| "agent_rewards": info["agent_rewards"], | |
| "per_agent_rewards": info["agent_rewards"], | |
| "done": done, | |
| "info": info, | |
| "conflict_rate": round(conflict_rate, 4), | |
| "conflicts": conflicts, | |
| }) | |
| def _filter_proposals(proposals: list, agent_obs_map: dict) -> list: | |
| """Drop any proposal where the agent claims a sensor not matching their type.""" | |
| filtered = [] | |
| used_sensors, used_targets = set(), set() | |
| for p in proposals: | |
| agent_id = p.agent_id | |
| agent_obs = agent_obs_map.get(agent_id) | |
| if not agent_obs: | |
| continue | |
| sensor_type = next((s["type"] for s in agent_obs.sensors if s["id"] == p.sensor_id), None) | |
| if sensor_type is None: | |
| continue | |
| if agent_id != "command" and sensor_type != agent_id: | |
| continue # wrong sensor type for this agent | |
| if p.sensor_id in used_sensors or p.target_id in used_targets: | |
| continue | |
| filtered.append(p) | |
| used_sensors.add(p.sensor_id) | |
| used_targets.add(p.target_id) | |
| return filtered | |
| def auto_multi(): | |
| global mx_obs | |
| if mx_obs is None: | |
| return jsonify({"error": "Call /reset_multi first"}), 400 | |
| if _has_adapters or _llm_client: | |
| print(f"[auto_multi] Using {'lora' if _has_adapters else 'llm'}", flush=True, file=sys.stderr) | |
| proposals, source = _get_multi_proposals(mx_obs) | |
| print(f"[auto_multi] Got {len(proposals)} proposals from {source}", flush=True, file=sys.stderr) | |
| else: | |
| # Use wired agent classes (not raw greedy helper) | |
| proposals = [] | |
| all_props = [] | |
| for agent_id, agent in [("satellite", sat_agent), ("drone", drone_agent), ("radar", radar_agent)]: | |
| agent.observe(mx_obs[agent_id]) | |
| all_props += agent.propose() | |
| command_agent.observe(mx_obs["command"], proposals=all_props) | |
| all_props += command_agent.propose() | |
| proposals = all_props | |
| source = "agents" | |
| new_obs, step_rewards, done, info = mx_env.step_multiagent(proposals) | |
| mx_obs = new_obs # keep obs even when done so state is readable | |
| conflict_rate = info["conflict_rate"] | |
| conflicts = info["conflicts"] | |
| return jsonify({ | |
| "proposals": [{"agent_id": p.agent_id, "sensor_id": p.sensor_id, | |
| "target_id": p.target_id} for p in proposals], | |
| "agent": source, | |
| "observations": {k: v.to_dict() for k, v in new_obs.items()}, | |
| "step_rewards": step_rewards, | |
| "agent_rewards": info["agent_rewards"], | |
| "per_agent_rewards": info["agent_rewards"], | |
| "done": done, | |
| "info": info, | |
| "conflict_rate": round(conflict_rate, 4), | |
| "conflicts": conflicts, | |
| }) | |
| # ── Metrics history endpoint ───────────────────────────────────────────────── | |
| def metrics_history(): | |
| metrics_path = Path("./logs/training_metrics.json") | |
| if not metrics_path.exists(): | |
| return jsonify([]) | |
| try: | |
| with open(metrics_path) as f: | |
| data = json.load(f) | |
| # Support both array format (per-episode) and legacy single-object format | |
| if isinstance(data, dict): | |
| data = [data] | |
| return jsonify(data) | |
| except Exception as e: | |
| return jsonify({"error": str(e)}), 500 | |
| # ── UI ──────────────────────────────────────────────────────────────────────── | |
| def ui(): | |
| return render_template("dashboard.html") | |
| def game(): | |
| return render_template("game.html") | |
| if __name__ == "__main__": | |
| port = int(os.environ.get("PORT", 7860)) | |
| app.run(host="0.0.0.0", port=port, debug=False, use_reloader=False) | |