Rename the project directories, Python distribution and import package, and npm package. Update launcher, Docker, tooling, tests, lockfiles, and documentation while preserving existing API contracts. Make AGENTS.md self-contained and remove external skill and workflow references.
559 lines
22 KiB
Python
559 lines
22 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
from typing import Any
|
|
from urllib.parse import urlencode
|
|
|
|
import httpx
|
|
|
|
from vocero_api.core import config as settings
|
|
from vocero_api.core import logging as app_logging
|
|
|
|
|
|
async def _fetch_reports_list(phone_number: str | None = None, auth_token: str | None = None) -> list | dict | None:
|
|
"""Fetch scheduled reports list from S2S_REPORT_LIST.
|
|
|
|
Completely generic / deploy-agnostic: performs GET ${S2S_REPORT_LIST} and appends
|
|
?phoneNumber=<x> if a phone is provided. Accepts JSON (optional {"result": [...]}
|
|
wrapper) and returns the list or unwrapped list. On any error logs and returns None
|
|
(never fails the proxy / reasoning tunnel).
|
|
|
|
USER FIX 2026-09-11: remove mass injection of scheduled reports.
|
|
• If S2S_INJECT_REPORTS is FALSE (DEFAULT) we short-circuit to None at the
|
|
very top — NO HTTP call is made to S2S_REPORT_LIST at all. The upstream
|
|
endpoint is never hit, not even for a "cheap empty list"; combined with
|
|
_resolve_reports_context → "" above, this guarantees ZERO scheduled-report
|
|
contents reach any LLM prompt anywhere (WS session instructions patch,
|
|
/api/reasoning user_context, reasoning instructions augment, quick reports
|
|
lookup response).
|
|
• S2S_INJECT_REPORTS=true restores prior behaviour (both the HTTP fetch AND
|
|
the full injection into every prompt path).
|
|
"""
|
|
# ── MASS-INJECTION KILL SWITCH (user request) ─────────────────────────
|
|
if not settings.S2S_INJECT_REPORTS:
|
|
return None
|
|
# ───────────────────────────────────────────────────────────────────────
|
|
if not settings.S2S_REPORT_LIST:
|
|
return None
|
|
if phone_number:
|
|
sep = "&" if "?" in settings.S2S_REPORT_LIST else "?"
|
|
target_url = f"{settings.S2S_REPORT_LIST}{sep}{urlencode({'phoneNumber': phone_number})}"
|
|
else:
|
|
target_url = settings.S2S_REPORT_LIST
|
|
app_logging.logger.info(
|
|
"[INJECT-REPORTS] fetch upstream URL=%s phoneLen=%s bearer=%s",
|
|
settings.S2S_REPORT_LIST,
|
|
len(phone_number) if phone_number else "none",
|
|
"yes" if auth_token else "no",
|
|
)
|
|
try:
|
|
headers = {}
|
|
if auth_token:
|
|
headers["Authorization"] = f"Bearer {auth_token}"
|
|
async with httpx.AsyncClient(timeout=15.0, follow_redirects=True) as http:
|
|
resp = await http.get(target_url, headers=headers or None)
|
|
if resp.status_code != 200:
|
|
app_logging.logger.warning(
|
|
"[INJECT-REPORTS] ⛔ upstream HTTP %s response=%.250s",
|
|
resp.status_code,
|
|
resp.text,
|
|
)
|
|
return None
|
|
text = (resp.text or "").strip()
|
|
if not text:
|
|
app_logging.logger.info("[INJECT-REPORTS] ⏭️ upstream returned empty body.")
|
|
return None
|
|
try:
|
|
payload = resp.json()
|
|
except Exception:
|
|
app_logging.logger.warning(
|
|
"[INJECT-REPORTS] ⛔ upstream returned non-JSON body len=%d; cannot parse list. response=%.250s",
|
|
len(text),
|
|
text,
|
|
)
|
|
return None
|
|
except Exception as exc:
|
|
app_logging.logger.warning(
|
|
"[INJECT-REPORTS] ⛔ fetch unreachable error=%r",
|
|
exc,
|
|
)
|
|
return None
|
|
# Unwrap {"result": [...]} or other common list-wrapping dict keys
|
|
# (e.g. {"data": [...], "reports": [...], "items": [...], "content": [...], "value": [...]}).
|
|
unwrapped: object = payload
|
|
if isinstance(payload, dict):
|
|
# Log full structure for debugging if enabled
|
|
if settings.S2S_DEBUG_PROMPT:
|
|
try:
|
|
keys_preview = ",".join(sorted(str(k) for k in payload.keys()))
|
|
preview = json.dumps(payload, ensure_ascii=False)
|
|
preview = preview[:4000]
|
|
app_logging.logger.info(
|
|
"[DEBUG-REPORTS][fetch] upstream response dict keys=[%s] full PREVIEW (len=%d):\n%s\n",
|
|
keys_preview,
|
|
len(preview),
|
|
preview,
|
|
)
|
|
except Exception:
|
|
pass
|
|
# Preferred explicit "result" key first
|
|
if "result" in payload and isinstance(payload["result"], list):
|
|
unwrapped = payload["result"]
|
|
else:
|
|
# Scan all common list-wrapper keys (case-insensitive match on the key)
|
|
wrapper_keys = {
|
|
k.lower()
|
|
for k in (
|
|
"data",
|
|
"reports",
|
|
"reportList",
|
|
"report_list",
|
|
"report",
|
|
"items",
|
|
"itemList",
|
|
"item_list",
|
|
"list",
|
|
"content",
|
|
"scheduled",
|
|
"scheduledReports",
|
|
"scheduled_reports",
|
|
"podcastList",
|
|
"podcast_list",
|
|
"podcasts",
|
|
"entries",
|
|
"records",
|
|
"rows",
|
|
"pageContent",
|
|
"page_content",
|
|
"value",
|
|
)
|
|
}
|
|
for k, v in payload.items():
|
|
if isinstance(k, str) and isinstance(v, list) and k.lower() in wrapper_keys:
|
|
unwrapped = v
|
|
break
|
|
# Fallback: if it's still a dict with only ONE list-valued key,
|
|
# assume that's the list (common pattern like {"data": [...]} with
|
|
# unusual key name we don't hardcode).
|
|
if not isinstance(unwrapped, list):
|
|
list_valued_keys = [k for k, v in payload.items() if isinstance(v, list) and len(v) > 0]
|
|
if len(list_valued_keys) == 1:
|
|
unwrapped = payload[list_valued_keys[0]]
|
|
# Validate array shape
|
|
if isinstance(unwrapped, list):
|
|
n_items = len(unwrapped)
|
|
n_dict = sum(1 for x in unwrapped if isinstance(x, dict))
|
|
app_logging.logger.info(
|
|
"[INJECT-REPORTS] ✅ reports list loaded OK (list len=%d dictItems=%d)",
|
|
n_items,
|
|
n_dict,
|
|
)
|
|
return unwrapped
|
|
app_logging.logger.info(
|
|
"[INJECT-REPORTS] ⏭️ upstream returned type=%s (not list); nothing to inject.",
|
|
type(unwrapped).__name__,
|
|
)
|
|
if settings.S2S_DEBUG_PROMPT and isinstance(payload, dict):
|
|
# Log final top-level keys to help diagnose wrapper shape
|
|
app_logging.logger.info(
|
|
"[DEBUG-REPORTS][fetch] wrapper not detected — top-level keys of dict response: [%s] "
|
|
"(list values under keys: %s)",
|
|
",".join(sorted(str(k) for k in payload.keys())),
|
|
",".join(sorted(str(k) + "=list[" + str(len(v)) + "]" for k, v in payload.items() if isinstance(v, list)))
|
|
or "none",
|
|
)
|
|
return None
|
|
|
|
|
|
def _extract_login_token(login_payload: dict | None) -> str | None:
|
|
"""Given the parsed response from the S2S_LOGIN_BY_PHONE endpoint (already
|
|
{"result":…}-unwrapped), try to extract an auth / session token.
|
|
Looks under common names (case-insensitive). Returns None if nothing found.
|
|
"""
|
|
if not login_payload or not isinstance(login_payload, dict):
|
|
return None
|
|
# Unwrap one level of nesting if result is still an dict: handled by caller already.
|
|
candidates = {
|
|
c.lower()
|
|
for c in (
|
|
"token",
|
|
"jwt",
|
|
"access_token",
|
|
"accessToken",
|
|
"auth_token",
|
|
"authToken",
|
|
"session_token",
|
|
"sessionToken",
|
|
"id_token",
|
|
"idToken",
|
|
"bearer",
|
|
"bearer_token",
|
|
"bearerToken",
|
|
"authentication_token",
|
|
)
|
|
}
|
|
for k, v in login_payload.items():
|
|
if isinstance(k, str) and k.lower() in candidates and isinstance(v, str) and v.strip():
|
|
return v.strip()
|
|
# Second pass: nested dict {"data": {"token": …}} or {"auth": {"jwt": …}}
|
|
for _k, v in login_payload.items():
|
|
if isinstance(v, dict):
|
|
nested = _extract_login_token(v)
|
|
if nested:
|
|
return nested
|
|
# Third pass: list like [{"token": ...}] (uncommon but safe)
|
|
for _k, v in login_payload.items():
|
|
if isinstance(v, list) and v:
|
|
for elem in v:
|
|
if isinstance(elem, dict):
|
|
nested = _extract_login_token(elem)
|
|
if nested:
|
|
return nested
|
|
return None
|
|
|
|
|
|
async def _login_by_phone(phone_number: str) -> tuple[str | None, dict | None]:
|
|
"""Perform login-by-phone: POST S2S_LOGIN_BY_PHONE with JSON
|
|
{"phoneNumber": phone_number}. Returns (bearer_token_or_None, response_obj_or_None).
|
|
|
|
Never fails: on any HTTP/parse error logs a warning and returns (None, None)
|
|
so subsequent profile/summary calls simply proceed without a Bearer token.
|
|
"""
|
|
if not settings.S2S_LOGIN_BY_PHONE or not phone_number:
|
|
return None, None
|
|
body = {"phoneNumber": phone_number}
|
|
app_logging.logger.info(
|
|
"[INJECT-LOGIN] POST upstream URL=%s phoneLen=%d",
|
|
settings.S2S_LOGIN_BY_PHONE,
|
|
len(phone_number),
|
|
)
|
|
try:
|
|
async with httpx.AsyncClient(timeout=10.0, follow_redirects=True) as http:
|
|
resp = await http.post(
|
|
settings.S2S_LOGIN_BY_PHONE,
|
|
json=body,
|
|
headers={"Content-Type": "application/json"},
|
|
)
|
|
if resp.status_code != 200:
|
|
app_logging.logger.warning(
|
|
"[INJECT-LOGIN] ⛔ upstream HTTP %s phoneLen=%d response=%.250s",
|
|
resp.status_code,
|
|
len(phone_number),
|
|
resp.text,
|
|
)
|
|
return None, None
|
|
text = (resp.text or "").strip()
|
|
if not text:
|
|
app_logging.logger.info("[INJECT-LOGIN] ⏭️ upstream returned empty body; no login token available.")
|
|
return None, None
|
|
try:
|
|
payload = resp.json()
|
|
except Exception:
|
|
app_logging.logger.warning(
|
|
"[INJECT-LOGIN] ⛔ upstream returned non-JSON body len=%d; cannot extract token. response=%.250s",
|
|
len(text),
|
|
text,
|
|
)
|
|
return None, None
|
|
except Exception as exc:
|
|
app_logging.logger.warning(
|
|
"[INJECT-LOGIN] ⛔ fetch unreachable phoneLen=%d error=%r",
|
|
len(phone_number),
|
|
exc,
|
|
)
|
|
return None, None
|
|
|
|
# Unwrap {"result": …} OR {"data": …} if present (common wrapper conventions).
|
|
unwrapped: dict | None = None
|
|
if isinstance(payload, dict):
|
|
for _wrap_key in ("result", "data", "payload"):
|
|
if isinstance(payload.get(_wrap_key), dict):
|
|
unwrapped = payload[_wrap_key]
|
|
break
|
|
if unwrapped is None:
|
|
unwrapped = payload
|
|
token = (
|
|
_extract_login_token(unwrapped)
|
|
if isinstance(unwrapped, dict)
|
|
else _extract_login_token(payload if isinstance(payload, dict) else None)
|
|
)
|
|
# Always log login-token diagnostics, even without S2S_DEBUG_PROMPT:
|
|
# token length, first eight characters, response keys and wrapper layer.
|
|
_all_keys: list[str] = []
|
|
if isinstance(payload, dict):
|
|
_all_keys.extend(str(k) for k in payload.keys())
|
|
_unwrap_keys: list[str] = []
|
|
if isinstance(unwrapped, dict):
|
|
_unwrap_keys.extend(str(k) for k in unwrapped.keys())
|
|
if isinstance(unwrapped, dict):
|
|
_t_preview = ""
|
|
_t_len = 0
|
|
if isinstance(token, str) and token.strip():
|
|
_t_len = len(token.strip())
|
|
if _t_len > 8:
|
|
_t_preview = token.strip()[:8] + "..."
|
|
else:
|
|
_t_preview = token.strip()
|
|
_fmt_note = ""
|
|
if _t_len >= 120:
|
|
_fmt_note = " [JWT-like]"
|
|
elif _t_len > 0:
|
|
_fmt_note = " [SHORT]"
|
|
_wrap_layer = "ROOT"
|
|
if isinstance(payload, dict):
|
|
for _wk in ("result", "data", "payload"):
|
|
if isinstance(payload.get(_wk), dict):
|
|
_wrap_layer = "result/data/payload"
|
|
break
|
|
_all_keys_s = ",".join(sorted(_all_keys)) if _all_keys else "-"
|
|
_unwrap_keys_s = ",".join(sorted(_unwrap_keys)) if _unwrap_keys else "-"
|
|
_t_info = "MISSING"
|
|
if _t_len:
|
|
_t_info = "YES len=" + str(_t_len) + " head=" + _t_preview
|
|
msg = f"[INJECT-LOGIN] ✅ login OK: wrapLayer={_wrap_layer} topKeys={_all_keys_s} unwrappedKeys={_unwrap_keys_s} token={_t_info}{_fmt_note}"
|
|
app_logging.logger.info("%s", msg)
|
|
return token, unwrapped
|
|
# Fallback: non-dict payload (string/list/number) — still log it, but no token expected.
|
|
app_logging.logger.info(
|
|
"[INJECT-LOGIN] ⏭️ login response parsed but type=%s (not dict); no token extracted.",
|
|
type(payload).__name__,
|
|
)
|
|
if settings.S2S_DEBUG_PROMPT:
|
|
app_logging.logger.info(
|
|
"[DEBUG-LOGIN][response] FULL login payload (len=%d):\n%s\n",
|
|
len(str(payload)),
|
|
payload,
|
|
)
|
|
return None, None
|
|
|
|
|
|
async def _fetch_latest_summary_for_phone(phone_number: str, auth_token: str | None = None) -> Any:
|
|
"""Fetch the latest caller summary via S2S_LATEST_SUMMARY URL.
|
|
|
|
Generic / completely deploy-agnostic: plain GET
|
|
S2S_LATEST_SUMMARY?phoneNumber=<phone>; accepts JSON (optional {"result": …}
|
|
wrapper) or plain text. Never fails the proxy / reasoning; returns None on
|
|
any error and logs it. If `auth_token` is provided it is sent as
|
|
Authorization: Bearer <auth_token>.
|
|
|
|
OVERRIDE (2026-09-23): when S2S_LATEST_SUMMARY_TEXT is set (non-empty) it
|
|
takes absolute priority over S2S_LATEST_SUMMARY. No HTTP call is made at all;
|
|
the raw plain-text value is returned directly. Designed for operators who want
|
|
a clean, pre-curated, essential summary snippet without any upstream API
|
|
dependency. Works even without a phone_number (static snippet).
|
|
"""
|
|
if settings.S2S_LATEST_SUMMARY_TEXT:
|
|
override_len = len(settings.S2S_LATEST_SUMMARY_TEXT)
|
|
app_logging.logger.info(
|
|
"[INJECT-SUMMARY] S2S_LATEST_SUMMARY_TEXT OVERRIDE active (len=%d chars). Skipping HTTP fetch; injecting raw static plain-text summary directly.",
|
|
override_len,
|
|
)
|
|
if settings.S2S_DEBUG_PROMPT:
|
|
app_logging.logger.info(
|
|
"[INJECT-SUMMARY] [DEBUG-PROMPT] S2S_LATEST_SUMMARY_TEXT raw value (first 800 chars):\n%s",
|
|
settings.S2S_LATEST_SUMMARY_TEXT[:800],
|
|
)
|
|
return settings.S2S_LATEST_SUMMARY_TEXT
|
|
if not settings.S2S_LATEST_SUMMARY or not phone_number:
|
|
return None
|
|
target_url = f"{settings.S2S_LATEST_SUMMARY}?{urlencode({'phoneNumber': phone_number})}"
|
|
app_logging.logger.info(
|
|
"[INJECT-SUMMARY] fetch upstream URL=%s phoneLen=%d bearer=%s",
|
|
settings.S2S_LATEST_SUMMARY,
|
|
len(phone_number),
|
|
"yes" if auth_token else "no",
|
|
)
|
|
try:
|
|
headers = {}
|
|
if auth_token:
|
|
headers["Authorization"] = f"Bearer {auth_token}"
|
|
async with httpx.AsyncClient(timeout=10.0, follow_redirects=True) as http:
|
|
resp = await http.get(target_url, headers=headers or None)
|
|
if resp.status_code != 200:
|
|
app_logging.logger.warning(
|
|
"[INJECT-SUMMARY] ⛔ upstream HTTP %s phoneLen=%d response=%.200s",
|
|
resp.status_code,
|
|
len(phone_number),
|
|
resp.text,
|
|
)
|
|
return None
|
|
# Accept JSON first, then fall back to plain text (some summary APIs
|
|
# return text/plain).
|
|
text = (resp.text or "").strip()
|
|
if not text:
|
|
return ""
|
|
try:
|
|
payload = resp.json()
|
|
except Exception:
|
|
app_logging.logger.info(
|
|
"[INJECT-SUMMARY] upstream returned non-JSON response (len=%d); treating as plain string summary.",
|
|
len(text),
|
|
)
|
|
return text
|
|
except Exception as exc:
|
|
app_logging.logger.warning(
|
|
"[INJECT-SUMMARY] ⛔ fetch unreachable phoneLen=%d error=%r",
|
|
len(phone_number),
|
|
exc,
|
|
)
|
|
return None
|
|
# Unwrap {"result": …} just like profile fetch (common wrapper pattern).
|
|
if isinstance(payload, dict) and "result" in payload:
|
|
payload = payload["result"]
|
|
# Filter out empty dict/list / empty string after unwrap.
|
|
empty = (
|
|
payload is None
|
|
or (isinstance(payload, str) and not payload.strip())
|
|
or (isinstance(payload, dict) and len(payload) == 0)
|
|
or (isinstance(payload, list) and len(payload) == 0)
|
|
)
|
|
if empty:
|
|
app_logging.logger.info("[INJECT-SUMMARY] ⏭️ upstream returned empty summary; nothing to inject.")
|
|
return None
|
|
if isinstance(payload, dict):
|
|
app_logging.logger.info(
|
|
"[INJECT-SUMMARY] ✅ summary loaded OK (object) keys=%s",
|
|
",".join(sorted(str(k) for k in payload.keys())),
|
|
)
|
|
elif isinstance(payload, list):
|
|
app_logging.logger.info(
|
|
"[INJECT-SUMMARY] ✅ summary loaded OK (list) len=%d",
|
|
len(payload),
|
|
)
|
|
else:
|
|
app_logging.logger.info(
|
|
"[INJECT-SUMMARY] ✅ summary loaded OK (string) len=%d",
|
|
len(str(payload)),
|
|
)
|
|
return payload
|
|
|
|
|
|
async def _fetch_podcast_episode(episode_id: str) -> Any:
|
|
"""Fetch a single podcast episode by ID from the Aquachile AzurianLab API.
|
|
|
|
Completely generic / deploy-agnostic: plain GET
|
|
https://aquachile.azurianlab.com/api/v1/podcast/episodes/<episode_id>
|
|
Accepts JSON (optional {"result": …} wrapper) or plain text. Never fails
|
|
the proxy / reasoning; returns None on any error and logs it.
|
|
"""
|
|
if not episode_id:
|
|
return None
|
|
target_url = f"https://aquachile.azurianlab.com/api/v1/podcast/episodes/{episode_id}"
|
|
app_logging.logger.info(
|
|
"[INJECT-EPISODE] fetch upstream URL=%s episodeId=%.80s",
|
|
target_url,
|
|
episode_id,
|
|
)
|
|
try:
|
|
async with httpx.AsyncClient(timeout=15.0, follow_redirects=True) as http:
|
|
resp = await http.get(target_url)
|
|
if resp.status_code != 200:
|
|
app_logging.logger.warning(
|
|
"[INJECT-EPISODE] ⛔ upstream HTTP %s episodeId=%.80s response=%.200s",
|
|
resp.status_code,
|
|
episode_id,
|
|
resp.text,
|
|
)
|
|
return None
|
|
text = (resp.text or "").strip()
|
|
if not text:
|
|
return ""
|
|
try:
|
|
payload = resp.json()
|
|
except Exception:
|
|
app_logging.logger.info(
|
|
"[INJECT-EPISODE] upstream returned non-JSON response (len=%d); treating as plain string.",
|
|
len(text),
|
|
)
|
|
return text
|
|
except Exception as exc:
|
|
app_logging.logger.warning(
|
|
"[INJECT-EPISODE] ⛔ fetch unreachable episodeId=%.80s error=%r",
|
|
episode_id,
|
|
exc,
|
|
)
|
|
return None
|
|
if isinstance(payload, dict) and "result" in payload:
|
|
payload = payload["result"]
|
|
empty = (
|
|
payload is None
|
|
or (isinstance(payload, str) and not payload.strip())
|
|
or (isinstance(payload, dict) and len(payload) == 0)
|
|
or (isinstance(payload, list) and len(payload) == 0)
|
|
)
|
|
if empty:
|
|
app_logging.logger.info("[INJECT-EPISODE] ⏭️ upstream returned empty episode; nothing to inject.")
|
|
return None
|
|
if isinstance(payload, dict):
|
|
app_logging.logger.info(
|
|
"[INJECT-EPISODE] ✅ episode loaded OK (object) keys=%s",
|
|
",".join(sorted(str(k) for k in payload.keys())),
|
|
)
|
|
elif isinstance(payload, list):
|
|
app_logging.logger.info(
|
|
"[INJECT-EPISODE] ✅ episode loaded OK (list) len=%d",
|
|
len(payload),
|
|
)
|
|
else:
|
|
app_logging.logger.info(
|
|
"[INJECT-EPISODE] ✅ episode loaded OK (string) len=%d",
|
|
len(str(payload)),
|
|
)
|
|
return payload
|
|
|
|
|
|
async def _fetch_user_profile_for_phone(phone_number: str, auth_token: str | None = None) -> dict | None:
|
|
"""Fetch the profile JSON from VOCERO_USER_PHONE_AUTH_URL for a phone.
|
|
Returns the raw {"result": ...}-unwrapped object or None. Errors are logged
|
|
then suppressed (never fail the proxy / reasoning because of this).
|
|
|
|
When `auth_token` is provided it is forwarded as Authorization: Bearer <auth_token>
|
|
to the upstream profile endpoint.
|
|
"""
|
|
if not settings.VOCERO_USER_PHONE_AUTH_URL or not phone_number:
|
|
return None
|
|
target_url = f"{settings.VOCERO_USER_PHONE_AUTH_URL}?{urlencode({'phoneNumber': phone_number})}"
|
|
app_logging.logger.info(
|
|
"[INJECT-PROFILE] fetch upstream URL=%s phoneLen=%d bearer=%s",
|
|
settings.VOCERO_USER_PHONE_AUTH_URL,
|
|
len(phone_number),
|
|
"yes" if auth_token else "no",
|
|
)
|
|
try:
|
|
headers = {}
|
|
if auth_token:
|
|
headers["Authorization"] = f"Bearer {auth_token}"
|
|
async with httpx.AsyncClient(timeout=10.0, follow_redirects=True) as http:
|
|
resp = await http.get(target_url, headers=headers or None)
|
|
if resp.status_code != 200:
|
|
app_logging.logger.info(
|
|
"[INJECT-PROFILE] ⛔ upstream HTTP %s phoneLen=%d response=%.200s",
|
|
resp.status_code,
|
|
len(phone_number),
|
|
resp.text,
|
|
)
|
|
return None
|
|
payload = resp.json()
|
|
except Exception as exc:
|
|
app_logging.logger.warning(
|
|
"[INJECT-PROFILE] ⛔ fetch unreachable phoneLen=%d error=%r",
|
|
len(phone_number),
|
|
exc,
|
|
)
|
|
return None
|
|
if isinstance(payload, dict) and "result" in payload and isinstance(payload["result"], dict):
|
|
profile_obj = payload["result"]
|
|
elif isinstance(payload, dict) and not isinstance(payload, list):
|
|
profile_obj = payload
|
|
else:
|
|
app_logging.logger.info(
|
|
"[INJECT-PROFILE] ⛔ upstream returned non-object JSON type=%s keys=?",
|
|
type(payload).__name__,
|
|
)
|
|
return None
|
|
app_logging.logger.info(
|
|
"[INJECT-PROFILE] ✅ profile loaded OK keys=%s",
|
|
",".join(sorted(profile_obj.keys())),
|
|
)
|
|
return profile_obj
|