Spaces:
Sleeping
Sleeping
Fix ASQ API response mapping
Browse filesCorrects the private ASQ Space initialize and chat API argument counts and response ordering, while retaining cold-start retries for phone-to-glasses interview setup.
README.md
CHANGED
|
@@ -35,6 +35,8 @@ The phone and glasses use different tokens. The permanent researcher code is nev
|
|
| 35 |
- `TARGET_CHAT_API_NAME`: defaults to `/chat_flow`.
|
| 36 |
- `SESSION_TTL_SECONDS`: defaults to 14,400 seconds.
|
| 37 |
- `MAX_BACKGROUND_CHARS`: defaults to 20,000 characters.
|
|
|
|
|
|
|
| 38 |
|
| 39 |
## Endpoints
|
| 40 |
|
|
|
|
| 35 |
- `TARGET_CHAT_API_NAME`: defaults to `/chat_flow`.
|
| 36 |
- `SESSION_TTL_SECONDS`: defaults to 14,400 seconds.
|
| 37 |
- `MAX_BACKGROUND_CHARS`: defaults to 20,000 characters.
|
| 38 |
+
- `PREPARE_TIMEOUT_SECONDS`: maximum cold-start retry window, defaults to 600 seconds.
|
| 39 |
+
- `PREPARE_RETRY_SECONDS`: delay between cold-start attempts, defaults to 15 seconds.
|
| 40 |
|
| 41 |
## Endpoints
|
| 42 |
|
app.py
CHANGED
|
@@ -3,7 +3,9 @@ import base64
|
|
| 3 |
import hashlib
|
| 4 |
import hmac
|
| 5 |
import json
|
|
|
|
| 6 |
import os
|
|
|
|
| 7 |
import secrets
|
| 8 |
import time
|
| 9 |
from typing import Any, Literal
|
|
@@ -25,6 +27,10 @@ TARGET_CHAT_API_NAME = os.getenv("TARGET_CHAT_API_NAME", "/chat_flow")
|
|
| 25 |
TARGET_INITIALIZE_API_NAME = os.getenv("TARGET_INITIALIZE_API_NAME", "/initialize_flow")
|
| 26 |
SESSION_TTL_SECONDS = int(os.getenv("SESSION_TTL_SECONDS", "14400"))
|
| 27 |
MAX_BACKGROUND_CHARS = int(os.getenv("MAX_BACKGROUND_CHARS", "20000"))
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
|
| 29 |
|
| 30 |
class AuthRequest(BaseModel):
|
|
@@ -118,32 +124,54 @@ def _bearer(authorization: str | None, roles: set[str] | None = None) -> tuple[d
|
|
| 118 |
return verify_token(authorization[7:], roles)
|
| 119 |
|
| 120 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
def extract_initial_state(result: Any) -> tuple[str, list[Any], int]:
|
| 122 |
-
|
|
|
|
|
|
|
| 123 |
raise RuntimeError("Unexpected initialization response from the ASQ Space")
|
| 124 |
-
hypotheses = str(result[
|
| 125 |
-
history = result[
|
| 126 |
-
turns =
|
| 127 |
if not hypotheses:
|
| 128 |
raise RuntimeError("ASQ returned no initial hypotheses")
|
| 129 |
return hypotheses, history, turns
|
| 130 |
|
| 131 |
|
| 132 |
def extract_question(result: Any) -> tuple[str, list[Any], int, str | None]:
|
| 133 |
-
|
|
|
|
|
|
|
| 134 |
raise RuntimeError("Unexpected response from the ASQ Space")
|
| 135 |
history = result[0]
|
| 136 |
-
turns =
|
| 137 |
-
hypotheses = str(result[
|
| 138 |
if not isinstance(history, list) or not history:
|
| 139 |
raise RuntimeError("ASQ returned no conversation history")
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
question = ""
|
| 147 |
if not question:
|
| 148 |
raise RuntimeError("ASQ returned an empty question")
|
| 149 |
return question, history, turns, hypotheses
|
|
@@ -165,8 +193,6 @@ def call_asq(transcript: str, session: dict[str, Any]) -> tuple[str, list[Any],
|
|
| 165 |
transcript,
|
| 166 |
session["history"],
|
| 167 |
session["background"],
|
| 168 |
-
session["hypotheses"],
|
| 169 |
-
session["turns"],
|
| 170 |
api_name=TARGET_CHAT_API_NAME,
|
| 171 |
)
|
| 172 |
return extract_question(result)
|
|
@@ -176,13 +202,22 @@ async def prepare_session(session_id: str, background: str) -> None:
|
|
| 176 |
session = sessions.get(session_id)
|
| 177 |
if not session:
|
| 178 |
return
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
if session_id in sessions:
|
| 187 |
session.update(
|
| 188 |
status="ready",
|
|
|
|
| 3 |
import hashlib
|
| 4 |
import hmac
|
| 5 |
import json
|
| 6 |
+
import logging
|
| 7 |
import os
|
| 8 |
+
import re
|
| 9 |
import secrets
|
| 10 |
import time
|
| 11 |
from typing import Any, Literal
|
|
|
|
| 27 |
TARGET_INITIALIZE_API_NAME = os.getenv("TARGET_INITIALIZE_API_NAME", "/initialize_flow")
|
| 28 |
SESSION_TTL_SECONDS = int(os.getenv("SESSION_TTL_SECONDS", "14400"))
|
| 29 |
MAX_BACKGROUND_CHARS = int(os.getenv("MAX_BACKGROUND_CHARS", "20000"))
|
| 30 |
+
PREPARE_TIMEOUT_SECONDS = int(os.getenv("PREPARE_TIMEOUT_SECONDS", "600"))
|
| 31 |
+
PREPARE_RETRY_SECONDS = int(os.getenv("PREPARE_RETRY_SECONDS", "15"))
|
| 32 |
+
|
| 33 |
+
logger = logging.getLogger("asq_gateway")
|
| 34 |
|
| 35 |
|
| 36 |
class AuthRequest(BaseModel):
|
|
|
|
| 124 |
return verify_token(authorization[7:], roles)
|
| 125 |
|
| 126 |
|
| 127 |
+
def _extract_turn_count(value: Any) -> int:
|
| 128 |
+
if isinstance(value, (int, float)):
|
| 129 |
+
return int(value)
|
| 130 |
+
match = re.search(r"\d+", str(value or ""))
|
| 131 |
+
return int(match.group(0)) if match else 0
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
def _message_text(message: Any) -> str:
|
| 135 |
+
if isinstance(message, str):
|
| 136 |
+
return message.strip()
|
| 137 |
+
if isinstance(message, dict):
|
| 138 |
+
content = message.get("content", message.get("text", ""))
|
| 139 |
+
return _message_text(content)
|
| 140 |
+
if isinstance(message, (list, tuple)):
|
| 141 |
+
parts = [_message_text(item) for item in message]
|
| 142 |
+
return "\n".join(part for part in parts if part).strip()
|
| 143 |
+
return str(message or "").strip()
|
| 144 |
+
|
| 145 |
+
|
| 146 |
def extract_initial_state(result: Any) -> tuple[str, list[Any], int]:
|
| 147 |
+
# /initialize_flow returns:
|
| 148 |
+
# turn markdown, interview history, hypotheses, summary, strategy, status.
|
| 149 |
+
if not isinstance(result, (list, tuple)) or len(result) < 6:
|
| 150 |
raise RuntimeError("Unexpected initialization response from the ASQ Space")
|
| 151 |
+
hypotheses = str(result[2] or "").strip()
|
| 152 |
+
history = result[1] if isinstance(result[1], list) else []
|
| 153 |
+
turns = _extract_turn_count(result[0])
|
| 154 |
if not hypotheses:
|
| 155 |
raise RuntimeError("ASQ returned no initial hypotheses")
|
| 156 |
return hypotheses, history, turns
|
| 157 |
|
| 158 |
|
| 159 |
def extract_question(result: Any) -> tuple[str, list[Any], int, str | None]:
|
| 160 |
+
# /chat_flow returns:
|
| 161 |
+
# history, cleared input, turn markdown, hypotheses, summary, strategy, status.
|
| 162 |
+
if not isinstance(result, (list, tuple)) or len(result) < 7:
|
| 163 |
raise RuntimeError("Unexpected response from the ASQ Space")
|
| 164 |
history = result[0]
|
| 165 |
+
turns = _extract_turn_count(result[2])
|
| 166 |
+
hypotheses = str(result[3]).strip() if result[3] is not None else None
|
| 167 |
if not isinstance(history, list) or not history:
|
| 168 |
raise RuntimeError("ASQ returned no conversation history")
|
| 169 |
+
assistant_messages = [
|
| 170 |
+
message
|
| 171 |
+
for message in history
|
| 172 |
+
if isinstance(message, dict) and message.get("role") == "assistant"
|
| 173 |
+
]
|
| 174 |
+
question = _message_text((assistant_messages or history)[-1])
|
|
|
|
| 175 |
if not question:
|
| 176 |
raise RuntimeError("ASQ returned an empty question")
|
| 177 |
return question, history, turns, hypotheses
|
|
|
|
| 193 |
transcript,
|
| 194 |
session["history"],
|
| 195 |
session["background"],
|
|
|
|
|
|
|
| 196 |
api_name=TARGET_CHAT_API_NAME,
|
| 197 |
)
|
| 198 |
return extract_question(result)
|
|
|
|
| 202 |
session = sessions.get(session_id)
|
| 203 |
if not session:
|
| 204 |
return
|
| 205 |
+
deadline = time.monotonic() + PREPARE_TIMEOUT_SECONDS
|
| 206 |
+
attempt = 0
|
| 207 |
+
while True:
|
| 208 |
+
attempt += 1
|
| 209 |
+
try:
|
| 210 |
+
hypotheses, history, turns = await asyncio.to_thread(initialize_asq, background)
|
| 211 |
+
break
|
| 212 |
+
except Exception as error:
|
| 213 |
+
remaining = deadline - time.monotonic()
|
| 214 |
+
logger.warning("ASQ preparation attempt %s failed: %s", attempt, type(error).__name__)
|
| 215 |
+
if remaining <= 0:
|
| 216 |
+
if session_id in sessions:
|
| 217 |
+
session["status"] = "error"
|
| 218 |
+
session["error"] = "Unable to start the model. Please try again."
|
| 219 |
+
return
|
| 220 |
+
await asyncio.sleep(min(PREPARE_RETRY_SECONDS, remaining))
|
| 221 |
if session_id in sessions:
|
| 222 |
session.update(
|
| 223 |
status="ready",
|