Spaces:
Sleeping
Sleeping
PROTHAM commited on
Commit Β·
96617e2
1
Parent(s): 9ec21a6
fix: change body to empty dict to prevent Pydantic 422 Unprocessable Entity errors during validation
Browse files- inference.py +191 -71
inference.py
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import os
|
| 2 |
import json
|
| 3 |
import asyncio
|
|
@@ -5,17 +14,29 @@ import httpx
|
|
| 5 |
from typing import List, Optional
|
| 6 |
from openai import OpenAI
|
| 7 |
|
|
|
|
| 8 |
API_BASE_URL = os.getenv("API_BASE_URL") or "https://router.huggingface.co/v1"
|
| 9 |
-
MODEL_NAME
|
| 10 |
-
API_KEY
|
| 11 |
-
ENV_URL
|
|
|
|
| 12 |
BENCHMARK = "api_debug_env"
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
SUCCESS_SCORE_THRESHOLD = 0.8
|
| 15 |
|
|
|
|
| 16 |
llm = OpenAI(base_url=API_BASE_URL, api_key=API_KEY)
|
| 17 |
|
| 18 |
-
SYSTEM_PROMPT = """
|
|
|
|
| 19 |
Respond ONLY in valid JSON with exactly these fields:
|
| 20 |
{
|
| 21 |
"method": "GET",
|
|
@@ -25,146 +46,245 @@ Respond ONLY in valid JSON with exactly these fields:
|
|
| 25 |
"query_params": {}
|
| 26 |
}
|
| 27 |
Rules:
|
| 28 |
-
- method must be uppercase
|
| 29 |
- url must start with /mock_api/
|
| 30 |
-
- body must be null for GET requests
|
| 31 |
- always include Content-Type: application/json when body is not null
|
| 32 |
-
- do not add any text outside the JSON
|
|
|
|
| 33 |
|
| 34 |
|
|
|
|
| 35 |
def log_start(task: str, env: str, model: str) -> None:
|
| 36 |
print(f"[START] task={task} env={env} model={model}", flush=True)
|
| 37 |
|
| 38 |
|
| 39 |
-
def log_step(step: int, action: str, reward: float, done: bool,
|
|
|
|
| 40 |
error_val = error if error else "null"
|
| 41 |
-
done_val
|
| 42 |
print(
|
| 43 |
-
f"[STEP] step={step} action={action} reward={reward:.2f}
|
|
|
|
| 44 |
flush=True,
|
| 45 |
)
|
| 46 |
|
| 47 |
|
| 48 |
-
def log_end(success: bool, steps: int, score: float,
|
|
|
|
| 49 |
rewards_str = ",".join(f"{r:.2f}" for r in rewards)
|
| 50 |
-
print(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
|
| 52 |
|
| 53 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
user_msg = (
|
| 55 |
-
f"Task
|
| 56 |
-
f"Broken request:\n{json.dumps(broken_request, indent=2)}\n\n"
|
| 57 |
f"Last response status: {last_status}\n"
|
| 58 |
-
f"Last response body: {str(last_body)[:
|
| 59 |
-
f"
|
| 60 |
-
f"
|
|
|
|
| 61 |
)
|
| 62 |
try:
|
| 63 |
resp = llm.chat.completions.create(
|
| 64 |
model=MODEL_NAME,
|
| 65 |
messages=[
|
| 66 |
{"role": "system", "content": SYSTEM_PROMPT},
|
| 67 |
-
{"role": "user",
|
| 68 |
],
|
| 69 |
-
max_tokens=
|
| 70 |
-
temperature=0.
|
| 71 |
)
|
| 72 |
-
raw = resp.choices[0].message.content.strip()
|
| 73 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
return json.loads(raw)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
except Exception:
|
| 76 |
-
|
|
|
|
|
|
|
|
|
|
| 77 |
|
| 78 |
|
|
|
|
| 79 |
async def env_reset(client: httpx.AsyncClient, task_id: str) -> dict:
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
|
| 84 |
|
| 85 |
async def env_step(client: httpx.AsyncClient, action: dict) -> dict:
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
|
| 96 |
|
|
|
|
| 97 |
async def run_task(client: httpx.AsyncClient, task_id: str) -> float:
|
| 98 |
-
rewards:
|
| 99 |
-
steps_taken = 0
|
| 100 |
-
score
|
| 101 |
-
success
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
|
| 103 |
log_start(task=task_id, env=BENCHMARK, model=MODEL_NAME)
|
| 104 |
|
| 105 |
try:
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
done
|
| 110 |
|
|
|
|
| 111 |
for step in range(1, max_steps + 1):
|
| 112 |
if done:
|
| 113 |
break
|
| 114 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
fixed = call_llm(
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
obs.get("last_status_code", 0),
|
| 119 |
-
obs.get("last_response_body", ""),
|
| 120 |
-
obs.get("step_feedback", ""),
|
| 121 |
)
|
| 122 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
action = {
|
| 124 |
-
"method":
|
| 125 |
-
"url":
|
| 126 |
-
"headers":
|
| 127 |
-
"body":
|
| 128 |
"query_params": dict(fixed.get("query_params") or {}),
|
| 129 |
}
|
| 130 |
|
| 131 |
-
action_str = f"{
|
| 132 |
-
result = await env_step(client, action)
|
| 133 |
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
|
| 138 |
rewards.append(reward)
|
| 139 |
steps_taken = step
|
| 140 |
|
| 141 |
-
log_step(step=step, action=action_str, reward=reward,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
|
| 143 |
if done:
|
| 144 |
break
|
| 145 |
|
| 146 |
-
|
|
|
|
|
|
|
|
|
|
| 147 |
success = score >= SUCCESS_SCORE_THRESHOLD
|
| 148 |
|
| 149 |
-
except Exception as
|
| 150 |
-
|
| 151 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
|
| 153 |
finally:
|
| 154 |
-
log_end(success=success, steps=steps_taken,
|
|
|
|
| 155 |
|
| 156 |
return score
|
| 157 |
|
| 158 |
|
| 159 |
-
|
|
|
|
| 160 |
async with httpx.AsyncClient(base_url=ENV_URL, timeout=30.0) as client:
|
| 161 |
-
# Verify
|
|
|
|
| 162 |
try:
|
| 163 |
-
health = await client.get("/health")
|
| 164 |
health.raise_for_status()
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 168 |
|
| 169 |
for task_id in ["easy", "medium", "hard"]:
|
| 170 |
await run_task(client, task_id)
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Inference Script for API Debug Environment
|
| 3 |
+
===========================================
|
| 4 |
+
Mandatory stdout format per Phase 2 validator:
|
| 5 |
+
[START] task=<task_name> env=<benchmark> model=<model_name>
|
| 6 |
+
[STEP] step=<n> action=<action_str> reward=<0.00> done=<true|false> error=<msg|null>
|
| 7 |
+
[END] success=<true|false> steps=<n> score=<score> rewards=<r1,r2,...,rn>
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
import os
|
| 11 |
import json
|
| 12 |
import asyncio
|
|
|
|
| 14 |
from typing import List, Optional
|
| 15 |
from openai import OpenAI
|
| 16 |
|
| 17 |
+
# ββ Environment variables ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 18 |
API_BASE_URL = os.getenv("API_BASE_URL") or "https://router.huggingface.co/v1"
|
| 19 |
+
MODEL_NAME = os.getenv("MODEL_NAME") or "mistralai/Mistral-7B-Instruct-v0.3"
|
| 20 |
+
API_KEY = os.getenv("HF_TOKEN") or os.getenv("API_KEY") or "dummy"
|
| 21 |
+
ENV_URL = os.getenv("ENV_URL", "http://localhost:7860").rstrip("/")
|
| 22 |
+
|
| 23 |
BENCHMARK = "api_debug_env"
|
| 24 |
+
|
| 25 |
+
# Max steps per difficulty (mirrors openenv.yaml config so we don't rely on /state)
|
| 26 |
+
TASK_MAX_STEPS = {
|
| 27 |
+
"easy": 5,
|
| 28 |
+
"medium": 5,
|
| 29 |
+
"hard": 10,
|
| 30 |
+
"expert": 10,
|
| 31 |
+
}
|
| 32 |
+
DEFAULT_MAX_STEPS = 5
|
| 33 |
SUCCESS_SCORE_THRESHOLD = 0.8
|
| 34 |
|
| 35 |
+
# ββ LLM client βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 36 |
llm = OpenAI(base_url=API_BASE_URL, api_key=API_KEY)
|
| 37 |
|
| 38 |
+
SYSTEM_PROMPT = """\
|
| 39 |
+
You are an HTTP API debugger. You receive a broken HTTP request and must fix it to get HTTP 200.
|
| 40 |
Respond ONLY in valid JSON with exactly these fields:
|
| 41 |
{
|
| 42 |
"method": "GET",
|
|
|
|
| 46 |
"query_params": {}
|
| 47 |
}
|
| 48 |
Rules:
|
| 49 |
+
- method must be uppercase (GET, POST, DELETE, etc.)
|
| 50 |
- url must start with /mock_api/
|
| 51 |
+
- body must be null for GET/DELETE requests
|
| 52 |
- always include Content-Type: application/json when body is not null
|
| 53 |
+
- do not add any text outside the JSON
|
| 54 |
+
- read the task description carefully and produce the correct fix on the first attempt"""
|
| 55 |
|
| 56 |
|
| 57 |
+
# ββ Logging helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 58 |
def log_start(task: str, env: str, model: str) -> None:
|
| 59 |
print(f"[START] task={task} env={env} model={model}", flush=True)
|
| 60 |
|
| 61 |
|
| 62 |
+
def log_step(step: int, action: str, reward: float, done: bool,
|
| 63 |
+
error: Optional[str]) -> None:
|
| 64 |
error_val = error if error else "null"
|
| 65 |
+
done_val = str(done).lower()
|
| 66 |
print(
|
| 67 |
+
f"[STEP] step={step} action={action} reward={reward:.2f}"
|
| 68 |
+
f" done={done_val} error={error_val}",
|
| 69 |
flush=True,
|
| 70 |
)
|
| 71 |
|
| 72 |
|
| 73 |
+
def log_end(success: bool, steps: int, score: float,
|
| 74 |
+
rewards: List[float]) -> None:
|
| 75 |
rewards_str = ",".join(f"{r:.2f}" for r in rewards)
|
| 76 |
+
print(
|
| 77 |
+
f"[END] success={str(success).lower()} steps={steps}"
|
| 78 |
+
f" score={score:.3f} rewards={rewards_str}",
|
| 79 |
+
flush=True,
|
| 80 |
+
)
|
| 81 |
|
| 82 |
|
| 83 |
+
# ββ LLM call ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 84 |
+
def call_llm(task_description: str, broken_request: dict,
|
| 85 |
+
last_status: int, last_body: str, feedback: str,
|
| 86 |
+
history: List[str]) -> dict:
|
| 87 |
+
"""Ask the LLM to fix the broken request. Returns a request dict."""
|
| 88 |
+
history_str = "\n".join(history[-3:]) if history else "None"
|
| 89 |
user_msg = (
|
| 90 |
+
f"Task description:\n{task_description}\n\n"
|
| 91 |
+
f"Broken request (JSON):\n{json.dumps(broken_request, indent=2)}\n\n"
|
| 92 |
f"Last response status: {last_status}\n"
|
| 93 |
+
f"Last response body: {str(last_body)[:400]}\n\n"
|
| 94 |
+
f"Server feedback: {feedback}\n\n"
|
| 95 |
+
f"Previous attempts (latest first):\n{history_str}\n\n"
|
| 96 |
+
f"Return the corrected request as JSON:"
|
| 97 |
)
|
| 98 |
try:
|
| 99 |
resp = llm.chat.completions.create(
|
| 100 |
model=MODEL_NAME,
|
| 101 |
messages=[
|
| 102 |
{"role": "system", "content": SYSTEM_PROMPT},
|
| 103 |
+
{"role": "user", "content": user_msg},
|
| 104 |
],
|
| 105 |
+
max_tokens=350,
|
| 106 |
+
temperature=0.0, # deterministic for API debugging
|
| 107 |
)
|
| 108 |
+
raw = (resp.choices[0].message.content or "").strip()
|
| 109 |
+
# Strip markdown code fences if present
|
| 110 |
+
if raw.startswith("```"):
|
| 111 |
+
raw = raw.split("```")[1]
|
| 112 |
+
if raw.startswith("json"):
|
| 113 |
+
raw = raw[4:]
|
| 114 |
+
raw = raw.strip()
|
| 115 |
return json.loads(raw)
|
| 116 |
+
except json.JSONDecodeError:
|
| 117 |
+
# LLM returned non-JSON β try to extract the first {...} block
|
| 118 |
+
try:
|
| 119 |
+
start = raw.index("{")
|
| 120 |
+
end = raw.rindex("}") + 1
|
| 121 |
+
return json.loads(raw[start:end])
|
| 122 |
+
except Exception:
|
| 123 |
+
pass
|
| 124 |
except Exception:
|
| 125 |
+
pass
|
| 126 |
+
# Last-resort fallback: return broken_request unchanged so the loop
|
| 127 |
+
# can still emit a [STEP] line with error context
|
| 128 |
+
return broken_request
|
| 129 |
|
| 130 |
|
| 131 |
+
# ββ Environment HTTP helpers βββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 132 |
async def env_reset(client: httpx.AsyncClient, task_id: str) -> dict:
|
| 133 |
+
"""POST /reset and return the parsed JSON body."""
|
| 134 |
+
try:
|
| 135 |
+
resp = await client.post(
|
| 136 |
+
"/reset",
|
| 137 |
+
json={"task_id": task_id},
|
| 138 |
+
timeout=30,
|
| 139 |
+
)
|
| 140 |
+
resp.raise_for_status()
|
| 141 |
+
return resp.json()
|
| 142 |
+
except httpx.HTTPStatusError as exc:
|
| 143 |
+
raise RuntimeError(f"env_reset HTTP error {exc.response.status_code}: "
|
| 144 |
+
f"{exc.response.text[:200]}") from exc
|
| 145 |
+
except Exception as exc:
|
| 146 |
+
raise RuntimeError(f"env_reset failed: {exc}") from exc
|
| 147 |
|
| 148 |
|
| 149 |
async def env_step(client: httpx.AsyncClient, action: dict) -> dict:
|
| 150 |
+
"""POST /step and return the parsed JSON body."""
|
| 151 |
+
try:
|
| 152 |
+
resp = await client.post(
|
| 153 |
+
"/step",
|
| 154 |
+
json={"action": action},
|
| 155 |
+
timeout=30,
|
| 156 |
+
)
|
| 157 |
+
resp.raise_for_status()
|
| 158 |
+
return resp.json()
|
| 159 |
+
except httpx.HTTPStatusError as exc:
|
| 160 |
+
raise RuntimeError(f"env_step HTTP error {exc.response.status_code}: "
|
| 161 |
+
f"{exc.response.text[:200]}") from exc
|
| 162 |
+
except Exception as exc:
|
| 163 |
+
raise RuntimeError(f"env_step failed: {exc}") from exc
|
| 164 |
|
| 165 |
|
| 166 |
+
# ββ Per-task episode runner ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 167 |
async def run_task(client: httpx.AsyncClient, task_id: str) -> float:
|
| 168 |
+
rewards: List[float] = []
|
| 169 |
+
steps_taken: int = 0
|
| 170 |
+
score: float = 0.001 # default: strictly > 0 for Phase 2
|
| 171 |
+
success: bool = False
|
| 172 |
+
history: List[str] = []
|
| 173 |
+
|
| 174 |
+
# Per-task max steps (matches openenv.yaml; /state doesn't expose these)
|
| 175 |
+
max_steps = TASK_MAX_STEPS.get(task_id, DEFAULT_MAX_STEPS)
|
| 176 |
|
| 177 |
log_start(task=task_id, env=BENCHMARK, model=MODEL_NAME)
|
| 178 |
|
| 179 |
try:
|
| 180 |
+
# ββ Reset ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 181 |
+
reset_resp = await env_reset(client, task_id)
|
| 182 |
+
obs = reset_resp.get("observation", reset_resp)
|
| 183 |
+
done = bool(reset_resp.get("done", False))
|
| 184 |
|
| 185 |
+
# ββ Step loop βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 186 |
for step in range(1, max_steps + 1):
|
| 187 |
if done:
|
| 188 |
break
|
| 189 |
|
| 190 |
+
# Build LLM prompt context
|
| 191 |
+
task_desc = obs.get("task_description", "")
|
| 192 |
+
broken_req = obs.get("broken_request", {})
|
| 193 |
+
last_status = obs.get("last_status_code", 0)
|
| 194 |
+
last_body = obs.get("last_response_body", "")
|
| 195 |
+
feedback = obs.get("step_feedback", "")
|
| 196 |
+
|
| 197 |
+
# Ask LLM to fix the request
|
| 198 |
fixed = call_llm(
|
| 199 |
+
task_desc, broken_req, last_status, last_body,
|
| 200 |
+
feedback, history,
|
|
|
|
|
|
|
|
|
|
| 201 |
)
|
| 202 |
|
| 203 |
+
# Sanitise the LLM output into a clean action dict
|
| 204 |
+
method = str(fixed.get("method", "GET")).upper()
|
| 205 |
+
url = str(fixed.get("url", "/mock_api/users"))
|
| 206 |
+
|
| 207 |
+
# Body must be None for GET/DELETE; otherwise keep what LLM says
|
| 208 |
+
body = fixed.get("body")
|
| 209 |
+
if method in ("GET", "DELETE", "HEAD"):
|
| 210 |
+
body = None
|
| 211 |
+
|
| 212 |
action = {
|
| 213 |
+
"method": method,
|
| 214 |
+
"url": url,
|
| 215 |
+
"headers": dict(fixed.get("headers") or {}),
|
| 216 |
+
"body": body if body is not None else {},
|
| 217 |
"query_params": dict(fixed.get("query_params") or {}),
|
| 218 |
}
|
| 219 |
|
| 220 |
+
action_str = f"{method}:{url}"
|
|
|
|
| 221 |
|
| 222 |
+
# ββ Step ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 223 |
+
try:
|
| 224 |
+
result = await env_step(client, action)
|
| 225 |
+
reward = float(result.get("reward", 0.0))
|
| 226 |
+
done = bool(result.get("done", False))
|
| 227 |
+
obs = result.get("observation", result)
|
| 228 |
+
error_str: Optional[str] = None
|
| 229 |
+
except RuntimeError as exc:
|
| 230 |
+
reward = 0.0
|
| 231 |
+
done = True # abort episode on env error
|
| 232 |
+
error_str = str(exc)[:120]
|
| 233 |
|
| 234 |
rewards.append(reward)
|
| 235 |
steps_taken = step
|
| 236 |
|
| 237 |
+
log_step(step=step, action=action_str, reward=reward,
|
| 238 |
+
done=done, error=error_str)
|
| 239 |
+
|
| 240 |
+
history.append(
|
| 241 |
+
f"step={step} action={action_str} "
|
| 242 |
+
f"status={obs.get('last_status_code', '?')} "
|
| 243 |
+
f"reward={reward:.2f}"
|
| 244 |
+
)
|
| 245 |
|
| 246 |
if done:
|
| 247 |
break
|
| 248 |
|
| 249 |
+
# Score: best single-step reward achieved during the episode.
|
| 250 |
+
# Phase 2 requires score strictly in (0, 1) β clamp away from boundaries.
|
| 251 |
+
_raw = max(rewards) if rewards else 0.0
|
| 252 |
+
score = max(0.001, min(0.999, _raw))
|
| 253 |
success = score >= SUCCESS_SCORE_THRESHOLD
|
| 254 |
|
| 255 |
+
except Exception as exc:
|
| 256 |
+
# Unexpected outer exception β still must emit [END]
|
| 257 |
+
print(f"[DEBUG] run_task({task_id}) outer exception: {exc}", flush=True)
|
| 258 |
+
if steps_taken == 0:
|
| 259 |
+
# We haven't emitted a single [STEP]; emit a dummy one so the
|
| 260 |
+
# Phase 2 validator can parse a proper log
|
| 261 |
+
log_step(step=1, action="error", reward=0.001, done=True,
|
| 262 |
+
error=str(exc)[:120])
|
| 263 |
+
steps_taken = 1
|
| 264 |
+
rewards = [0.001]
|
| 265 |
|
| 266 |
finally:
|
| 267 |
+
log_end(success=success, steps=steps_taken,
|
| 268 |
+
score=score, rewards=rewards)
|
| 269 |
|
| 270 |
return score
|
| 271 |
|
| 272 |
|
| 273 |
+
# ββ Entry point βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 274 |
+
async def main() -> None:
|
| 275 |
async with httpx.AsyncClient(base_url=ENV_URL, timeout=30.0) as client:
|
| 276 |
+
# Verify env is reachable β log warning but ALWAYS proceed so that
|
| 277 |
+
# mandatory [START]/[END] lines are emitted for every task.
|
| 278 |
try:
|
| 279 |
+
health = await client.get("/health", timeout=10)
|
| 280 |
health.raise_for_status()
|
| 281 |
+
print(f"[DEBUG] Environment reachable at {ENV_URL}", flush=True)
|
| 282 |
+
except Exception as exc:
|
| 283 |
+
print(
|
| 284 |
+
f"[DEBUG] Environment not reachable at {ENV_URL}: {exc}",
|
| 285 |
+
flush=True,
|
| 286 |
+
)
|
| 287 |
+
# Do NOT return β we must still run tasks to emit required log lines
|
| 288 |
|
| 289 |
for task_id in ["easy", "medium", "hard"]:
|
| 290 |
await run_task(client, task_id)
|