LucasUn commited on
Commit
67eb04e
·
verified ·
1 Parent(s): 47d4618

Fix ASQ API response mapping

Browse files

Corrects the private ASQ Space initialize and chat API argument counts and response ordering, while retaining cold-start retries for phone-to-glasses interview setup.

Files changed (2) hide show
  1. README.md +2 -0
  2. app.py +58 -23
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
- if not isinstance(result, (list, tuple)) or len(result) < 4:
 
 
123
  raise RuntimeError("Unexpected initialization response from the ASQ Space")
124
- hypotheses = str(result[0] or "").strip()
125
- history = result[3] if isinstance(result[3], list) else []
126
- turns = int(result[1] or 0)
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
- if not isinstance(result, (list, tuple)) or len(result) < 4:
 
 
134
  raise RuntimeError("Unexpected response from the ASQ Space")
135
  history = result[0]
136
- turns = int(result[3])
137
- hypotheses = str(result[2]).strip() if result[2] is not None else None
138
  if not isinstance(history, list) or not history:
139
  raise RuntimeError("ASQ returned no conversation history")
140
- last = history[-1]
141
- if isinstance(last, dict):
142
- question = str(last.get("content", "")).strip()
143
- elif isinstance(last, (list, tuple)) and len(last) >= 2:
144
- question = str(last[1]).strip()
145
- else:
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
- try:
180
- hypotheses, history, turns = await asyncio.to_thread(initialize_asq, background)
181
- except Exception:
182
- if session_id in sessions:
183
- session["status"] = "error"
184
- session["error"] = "Unable to start the model. Please try again."
185
- return
 
 
 
 
 
 
 
 
 
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",