chore(vocero): remove the legacy ApiPilot integration (#3)
Vocero now retrieves AquaPortal content through authenticated MCP. This removes the obsolete ApiPilot integration from the API and browser while preserving the separately deployed speech pipeline. Release notes: - Removed ApiPilot endpoints, configuration, credential overrides, and HTTP/realtime tool execution. - Removed browser ApiPilot catalog loading and prompt generation while retaining authenticated MCP discovery. - Updated regression coverage and operating documentation for the retired routes and settings. Reviewed-on: AquaPortal/s2s#3
This commit is contained in:
@@ -13,12 +13,10 @@ from websockets.typing import Subprotocol
|
||||
from vocero_api.core import config as settings
|
||||
from vocero_api.core import logging as app_logging
|
||||
from vocero_api.core import urls as core_urls
|
||||
from vocero_api.providers import apipilot as providers_apipilot
|
||||
from vocero_api.providers import caller as providers_caller
|
||||
from vocero_api.realtime import events as realtime_events
|
||||
from vocero_api.realtime.mcp_proxy import realtime_mcp_proxy
|
||||
from vocero_api.services import context as services_context
|
||||
from vocero_api.services import tool_context as services_tool_context
|
||||
|
||||
|
||||
async def realtime_proxy(ws: WebSocket) -> None:
|
||||
@@ -46,102 +44,20 @@ async def realtime_proxy(ws: WebSocket) -> None:
|
||||
injected_profile_context = ""
|
||||
injected_reports_context = ""
|
||||
injected_summary_context = ""
|
||||
injected_apipilot_pass1 = ""
|
||||
apipilot_tools_pass1: list = []
|
||||
inject_phone = ""
|
||||
inject_episode_id = ""
|
||||
client_query = {}
|
||||
login_token: str | None = None
|
||||
login_response_obj: dict | None = None
|
||||
profile_obj: dict | None = None
|
||||
needs_apipilot = bool(settings.S2S_APIPILOT_TOOLS_URL) or bool(settings.S2S_APIPILOT_TOOLS_EXECUTE_URL)
|
||||
try:
|
||||
client_query = dict(parse_qsl(ws.url.query, keep_blank_values=True))
|
||||
except Exception: # noqa: BLE001
|
||||
client_query = {}
|
||||
|
||||
# ApiPilot works independently of profile injection. When enabled, extract
|
||||
# phone/episode query parameters, resolve the bearer for authenticated tools,
|
||||
# and load the profile for email defaults when its endpoint is configured.
|
||||
if needs_apipilot:
|
||||
inject_phone = (client_query.get("phoneNumber") or "").strip() or inject_phone
|
||||
inject_episode_id = (client_query.get("episode_id") or "").strip() or inject_episode_id
|
||||
_needs_login_apipilot = bool(settings.S2S_LOGIN_BY_PHONE.strip())
|
||||
_needs_profile_apipilot = bool(settings.VOCERO_USER_PHONE_AUTH_URL.strip())
|
||||
# Always log initial WS state to diagnose missing phone, token or login
|
||||
# configuration that could leave the AquaPortal user anonymous.
|
||||
_s_init = {
|
||||
"needs_apipilot": needs_apipilot,
|
||||
"phoneLen": len(inject_phone or ""),
|
||||
"phoneHead": (
|
||||
("+" + inject_phone.lstrip("+")[:3] + "...")
|
||||
if (inject_phone and len(inject_phone) > 6)
|
||||
else (inject_phone or "")
|
||||
),
|
||||
"loginTokenLen": (len(login_token) if isinstance(login_token, str) and login_token.strip() else 0),
|
||||
"loginByPhone": ("YES" if _needs_login_apipilot else "NO"),
|
||||
"profileUrl": ("YES" if _needs_profile_apipilot else "NO"),
|
||||
"S2S_INJECT_PROFILE": settings.S2S_INJECT_PROFILE,
|
||||
}
|
||||
app_logging.logger.info(
|
||||
"[INJECT-APIPILOT][WS connect] INITIAL STATE: " + ", ".join(f"{k}={v}" for k, v in sorted(_s_init.items())),
|
||||
)
|
||||
# The API-key bypass makes login_by_phone unnecessary.
|
||||
if (
|
||||
not settings.S2S_APIPILOT_API_KEY
|
||||
and _needs_login_apipilot
|
||||
and inject_phone
|
||||
and (
|
||||
not isinstance(login_token, str)
|
||||
or not login_token.strip()
|
||||
or (isinstance(login_token, str) and 0 < len(login_token.strip()) < 80)
|
||||
)
|
||||
):
|
||||
try:
|
||||
login_token, _lresp = await providers_caller._login_by_phone(inject_phone)
|
||||
if isinstance(_lresp, dict) and _lresp:
|
||||
login_response_obj = _lresp
|
||||
app_logging.logger.info(
|
||||
"[INJECT-APIPILOT][WS connect] login_by_phone for authenticated ApiPilot execution phoneLen=%d token=%s",
|
||||
len(inject_phone or ""),
|
||||
f"YES len={len(login_token or '')}"
|
||||
if isinstance(login_token, str) and login_token
|
||||
else "NONE (403!)",
|
||||
)
|
||||
except Exception as _exc_login: # noqa: BLE001
|
||||
app_logging.logger.info(
|
||||
"[INJECT-APIPILOT][WS connect] login_by_phone failed (anonymous ApiPilot user, likely 403) phoneLen=%d err=%r",
|
||||
len(inject_phone or ""),
|
||||
_exc_login,
|
||||
)
|
||||
if _needs_profile_apipilot and inject_phone and (not isinstance(profile_obj, dict) or not profile_obj):
|
||||
try:
|
||||
profile_obj = await providers_caller._fetch_user_profile_for_phone(inject_phone, auth_token=login_token)
|
||||
app_logging.logger.info(
|
||||
"[INJECT-APIPILOT][WS connect] profile fetch for email defaults phoneLen=%d profile=%s",
|
||||
len(inject_phone or ""),
|
||||
("FOUND user=%s" % (((profile_obj or {}).get("user") or {}).get("name") or "?"))
|
||||
if isinstance(profile_obj, dict)
|
||||
else "NONE",
|
||||
)
|
||||
except Exception as _exc_p: # noqa: BLE001
|
||||
app_logging.logger.info(
|
||||
"[INJECT-APIPILOT][WS connect] profile fetch failed phoneLen=%d err=%r",
|
||||
len(inject_phone or ""),
|
||||
_exc_p,
|
||||
)
|
||||
|
||||
if not needs_apipilot:
|
||||
app_logging.logger.info(
|
||||
"[INJECT-APIPILOT] ⏭️ WS connect: APIPILOT tools injection DISABLED (S2S_APIPILOT_TOOLS_URL='%s' S2S_APIPILOT_TOOLS_EXECUTE_URL='%s'). "
|
||||
"No ApiPilot fetch or session.update patch in this tunnel.",
|
||||
settings.S2S_APIPILOT_TOOLS_URL,
|
||||
settings.S2S_APIPILOT_TOOLS_EXECUTE_URL,
|
||||
)
|
||||
if not settings.S2S_INJECT_PROFILE:
|
||||
app_logging.logger.info(
|
||||
"[INJECT-PROFILE] ⏭️ WS connect: SKIPPED (S2S_INJECT_PROFILE=false). "
|
||||
"No profile/episode fetch; tunnel stays pure passthrough for injects EXCEPT apipilot."
|
||||
"No profile/episode fetch; tunnel stays pure passthrough for context injection."
|
||||
)
|
||||
else:
|
||||
needs_phone_authed = bool(settings.VOCERO_USER_PHONE_AUTH_URL.strip())
|
||||
@@ -177,7 +93,7 @@ async def realtime_proxy(ws: WebSocket) -> None:
|
||||
# 0) Login-by-phone (optional — only if S2S_LOGIN_BY_PHONE env is set).
|
||||
# Obtains a bearer token used on subsequent profile/reports/summary fetches.
|
||||
if needs_login and inject_phone:
|
||||
login_token, login_response_obj = await providers_caller._login_by_phone(inject_phone)
|
||||
login_token, _ = await providers_caller._login_by_phone(inject_phone)
|
||||
# 0.5) Podcast episode fetch (independent of phone — uses only episode_id from query)
|
||||
if inject_episode_id:
|
||||
if settings.S2S_DEBUG_PROMPT:
|
||||
@@ -259,121 +175,12 @@ async def realtime_proxy(ws: WebSocket) -> None:
|
||||
app_logging.logger.info(
|
||||
"[INJECT-SUMMARY] ⏭️ WS connect: latest summary skipped (context empty / upstream returned empty summary / fetch failed)."
|
||||
)
|
||||
# 3) Fetch the ApiPilot pass-1 tool list independently of S2S_INJECT_PROFILE,
|
||||
# even without login/phone. Use the resolved token or anonymous fingerprint,
|
||||
# matching the reasoning endpoint's server cache.
|
||||
if needs_apipilot:
|
||||
apipilot_auth_header = f"Bearer {login_token}" if isinstance(login_token, str) and login_token.strip() else None
|
||||
# Phone login resolves the AquaPortal JWT for execution. The configured
|
||||
# S2S_APIPILOT_SERVICE_TOKEN overrides it.
|
||||
if settings.S2S_APIPILOT_SERVICE_TOKEN:
|
||||
_svc = settings.S2S_APIPILOT_SERVICE_TOKEN.strip()
|
||||
if _svc:
|
||||
apipilot_auth_header = f"Bearer {_svc}" if not _svc.lower().startswith("bearer ") else _svc
|
||||
demo_session_key = client_query.get("x-demo-session") if isinstance(client_query, dict) else None
|
||||
if not demo_session_key:
|
||||
demo_session_key = f"anon-ws-{client_ip}-{id(ws)}"
|
||||
try:
|
||||
# Always log discovery, matching reasoning pass 1.
|
||||
app_logging.logger.info(
|
||||
"[INJECT-APIPILOT] WS connect: fetching tools (auth=%s demoSession=%.40s...) upstream=%s...",
|
||||
"yes" if apipilot_auth_header else "anon",
|
||||
(demo_session_key[:40] if isinstance(demo_session_key, str) else "?"),
|
||||
settings.S2S_APIPILOT_TOOLS_URL or "<using override execute>",
|
||||
)
|
||||
if settings.S2S_DEBUG_PROMPT:
|
||||
app_logging.logger.info(
|
||||
"[DEBUG-APIPILOT][WS connect] tools fetch detail toolsURL=%s executeURL=%s overrideExec=%s",
|
||||
settings.S2S_APIPILOT_TOOLS_URL,
|
||||
settings.S2S_APIPILOT_TOOLS_EXECUTE_URL,
|
||||
providers_apipilot._apipilot_resolve_execute_url(),
|
||||
)
|
||||
_t0 = providers_apipilot._monotonic_now()
|
||||
apipilot_tools_pass1 = (
|
||||
await providers_apipilot._apipilot_fetch_tools(
|
||||
auth_header=apipilot_auth_header,
|
||||
demo_session=demo_session_key if isinstance(demo_session_key, str) else None,
|
||||
refresh=False,
|
||||
)
|
||||
or []
|
||||
)
|
||||
_el = providers_apipilot._monotonic_now() - _t0
|
||||
# Supply known phone/episode defaults to pass 1 so the model does
|
||||
# not leave required arguments empty (observed ARGUMENTS=>>> bug).
|
||||
_apipilot_defaults_p1: dict[str, str] = {}
|
||||
if isinstance(inject_phone, str) and inject_phone.strip():
|
||||
_apipilot_defaults_p1["phone"] = inject_phone.strip()
|
||||
if isinstance(inject_episode_id, str) and inject_episode_id.strip():
|
||||
_apipilot_defaults_p1["episode_id"] = inject_episode_id.strip()
|
||||
# Also supply email when the resolved profile has a valid value.
|
||||
if isinstance(profile_obj, dict) and profile_obj:
|
||||
try:
|
||||
u = profile_obj.get("user") if isinstance(profile_obj.get("user"), dict) else None
|
||||
if isinstance(u, dict):
|
||||
email = cast(str, u.get("email")) if isinstance(u.get("email"), str) else ""
|
||||
if email.strip():
|
||||
_apipilot_defaults_p1["email"] = email.strip()
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
session_ctx_apipilot_defaults = dict(_apipilot_defaults_p1)
|
||||
injected_apipilot_pass1 = (
|
||||
services_tool_context._resolve_apipilot_reasoning_context_block(
|
||||
tools_list=apipilot_tools_pass1,
|
||||
context_defaults=_apipilot_defaults_p1,
|
||||
)
|
||||
or ""
|
||||
)
|
||||
if injected_apipilot_pass1.strip():
|
||||
app_logging.logger.info(
|
||||
"[APIPILOT] REASONING PASS-1 (WS realtime): tool list ready N=%d t=%.0fms auth=%s defaults=%s",
|
||||
len(apipilot_tools_pass1),
|
||||
_el,
|
||||
"Y" if isinstance(apipilot_auth_header, str) else "anon",
|
||||
list(_apipilot_defaults_p1.keys()),
|
||||
)
|
||||
app_logging.logger.info(
|
||||
"[INJECT-APIPILOT] ✅ WS connect: APIPILOT TOOLS list PASS-1 READY Ntools=%d wrappedLen=%d defaultsPhone=%s -> will patch session.update frames in this tunnel.",
|
||||
len(apipilot_tools_pass1),
|
||||
len(injected_apipilot_pass1),
|
||||
("yes" if _apipilot_defaults_p1.get("phone") else "no"),
|
||||
)
|
||||
else:
|
||||
app_logging.logger.info(
|
||||
"[INJECT-APIPILOT] ⏭️ WS connect: apipilot fetch returned empty tools list; PASS-1 injection skipped for this tunnel.",
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
app_logging.logger.info(
|
||||
"[INJECT-APIPILOT] ⛔ WS connect: apipilot tools fetch failed. Skipping PASS-1 injection for tunnel. err=%r",
|
||||
exc,
|
||||
)
|
||||
injected_apipilot_pass1 = ""
|
||||
apipilot_tools_pass1 = []
|
||||
session_ctx_apipilot_defaults = {}
|
||||
# Preserve the exact discovery authentication and demo-session values.
|
||||
# AquaPortal binds authorized tools to GET /tools' x-demo-session; a new
|
||||
# ws-pass2 value would appear anonymous and produce a 403 response.
|
||||
_ws_apipilot_auth_for_execute = apipilot_auth_header
|
||||
_ws_apipilot_demo_for_execute = demo_session_key if isinstance(demo_session_key, str) else None
|
||||
# Preserve API-key identity in every propagation variable, matching the
|
||||
# discovery cache fingerprint and HTTP headers even during pass-2 fallback.
|
||||
if settings.S2S_APIPILOT_API_KEY:
|
||||
_ws_apipilot_auth_for_execute = f"Bearer {settings.S2S_APIPILOT_API_KEY}"
|
||||
_ws_apipilot_demo_for_execute = settings.S2S_APIPILOT_API_KEY
|
||||
else:
|
||||
# With ApiPilot unconfigured, keep defaults as an empty dict rather than None.
|
||||
session_ctx_apipilot_defaults = {}
|
||||
injected_apipilot_pass1 = ""
|
||||
apipilot_tools_pass1 = []
|
||||
_ws_apipilot_auth_for_execute = None
|
||||
_ws_apipilot_demo_for_execute = None
|
||||
app_logging.logger.info(
|
||||
"[INJECT-PROFILE] WS connect FINAL: injectEpisode=%s injectProfile=%s injectReports=%s injectSummary=%s injectApipilotPass1=%s(Ntools=%d) login=%s episodeIdLen=%d phoneLen=%d episodeCtxLen=%d profileCtxLen=%d reportsCtxLen=%d summaryCtxLen=%d apipilotCtxLen=%d target=%s",
|
||||
"[INJECT-PROFILE] WS connect FINAL: injectEpisode=%s injectProfile=%s injectReports=%s injectSummary=%s login=%s episodeIdLen=%d phoneLen=%d episodeCtxLen=%d profileCtxLen=%d reportsCtxLen=%d summaryCtxLen=%d target=%s",
|
||||
bool(injected_episode_context),
|
||||
bool(injected_profile_context),
|
||||
bool(injected_reports_context),
|
||||
bool(injected_summary_context),
|
||||
bool(injected_apipilot_pass1.strip()),
|
||||
len(apipilot_tools_pass1),
|
||||
("token-present" if login_token else "skipped"),
|
||||
len(inject_episode_id),
|
||||
len(inject_phone),
|
||||
@@ -381,7 +188,6 @@ async def realtime_proxy(ws: WebSocket) -> None:
|
||||
len(injected_profile_context),
|
||||
len(injected_reports_context),
|
||||
len(injected_summary_context),
|
||||
len(injected_apipilot_pass1),
|
||||
upstream_url,
|
||||
)
|
||||
if settings.S2S_DEBUG_PROMPT and injected_episode_context.strip():
|
||||
@@ -415,11 +221,6 @@ async def realtime_proxy(ws: WebSocket) -> None:
|
||||
"injected_profile_context": injected_profile_context,
|
||||
"injected_reports_context": injected_reports_context,
|
||||
"injected_summary_context": injected_summary_context,
|
||||
"injected_apipilot_pass1": injected_apipilot_pass1,
|
||||
"inject_phone": inject_phone,
|
||||
"inject_episode_id": inject_episode_id,
|
||||
"login_token": login_token,
|
||||
"login_response": login_response_obj,
|
||||
# 2026-09-11 proper-name anglicization guard: keep the full
|
||||
# upstream profile object so:
|
||||
# (a) _build_voice_rules_head_lock_block can emit rule #5
|
||||
@@ -429,24 +230,6 @@ async def realtime_proxy(ws: WebSocket) -> None:
|
||||
# Alessandro with word-boundary regex even when the
|
||||
# prompt lock fails on GPTQ.
|
||||
"profile_obj": profile_obj,
|
||||
"apipilot_tools_pass1": apipilot_tools_pass1,
|
||||
# Accumulate transcripts per response_id and prevent pass-2
|
||||
# injection loops after ApiPilot execution.
|
||||
"_ws_apipilot_asst_transcript_acc": {}, # response_id -> str full transcript
|
||||
"_ws_apipilot_pass2_active": False, # True during the second response; prevents infinite marker loops.
|
||||
"_ws_apipilot_pass2_count": 0, # Session execution count, limited to three attempts.
|
||||
# Defaults prepared by pass 1 fill missing pass-2 arguments.
|
||||
"apipilot_defaults": (session_ctx_apipilot_defaults if needs_apipilot else {}),
|
||||
# Pass-2 phone fallback also works when defaults are empty but
|
||||
# the query string supplied phoneNumber.
|
||||
"inject_phone": (inject_phone if isinstance(inject_phone, str) else ""), # noqa: F601
|
||||
# Bearer for authenticated execution, resolved by phone login
|
||||
# independently of S2S_INJECT_PROFILE.
|
||||
"login_token": (login_token if isinstance(login_token, str) else None), # noqa: F601
|
||||
# Execute with the exact auth/demo values used for WS discovery;
|
||||
# AquaPortal binds the authorized tool list to that demo session.
|
||||
"apipilot_exec_auth": _ws_apipilot_auth_for_execute,
|
||||
"apipilot_exec_demo": _ws_apipilot_demo_for_execute,
|
||||
}
|
||||
client_to_upstream = asyncio.create_task(_client_to_upstream(ws, upstream, client_ip, session_ctx))
|
||||
upstream_to_client = asyncio.create_task(_upstream_to_client(ws, upstream, client_ip, session_ctx))
|
||||
@@ -542,7 +325,6 @@ async def _client_to_upstream(
|
||||
ctx_profile_context = session_ctx.get("injected_profile_context") if isinstance(session_ctx, dict) else ""
|
||||
ctx_reports_context = session_ctx.get("injected_reports_context") if isinstance(session_ctx, dict) else ""
|
||||
ctx_summary_context = session_ctx.get("injected_summary_context") if isinstance(session_ctx, dict) else ""
|
||||
ctx_apipilot_pass1 = session_ctx.get("injected_apipilot_pass1") if isinstance(session_ctx, dict) else ""
|
||||
ctx_profile_obj = session_ctx.get("profile_obj") if isinstance(session_ctx, dict) else None
|
||||
forward_text = services_context._patch_client_session_update_with_profile(
|
||||
message["text"],
|
||||
@@ -551,7 +333,6 @@ async def _client_to_upstream(
|
||||
ctx_summary_context or "",
|
||||
ctx_episode_context or "",
|
||||
profile_obj=ctx_profile_obj,
|
||||
apipilot_context_text_pass1=ctx_apipilot_pass1 or "",
|
||||
)
|
||||
await upstream.send(forward_text)
|
||||
elif message.get("bytes") is not None:
|
||||
@@ -576,38 +357,6 @@ async def _upstream_to_client(
|
||||
) -> None:
|
||||
async for message in upstream:
|
||||
if isinstance(message, bytes):
|
||||
# Drop audio only for the current response_id marked as containing
|
||||
# a CALL marker: Qwen3 may already have spoken it. Matching any ID
|
||||
# would also discard valid pass-2 audio.
|
||||
drop_now = False
|
||||
matched_rid = ""
|
||||
drop_ids = session_ctx.get("_ws_drop_audio_for_response_ids")
|
||||
if isinstance(drop_ids, dict) and drop_ids:
|
||||
rid_ctx = session_ctx.get("_ws_current_response_id")
|
||||
if isinstance(rid_ctx, str) and rid_ctx and drop_ids.get(rid_ctx):
|
||||
drop_now = True
|
||||
matched_rid = rid_ctx
|
||||
elif (not isinstance(rid_ctx, str) or not rid_ctx) and len(drop_ids) == 1:
|
||||
# Fallback when no current ID exists but exactly one response
|
||||
# remains in the dictionary.
|
||||
only_rid = next(iter(drop_ids.keys()))
|
||||
if drop_ids.get(only_rid):
|
||||
drop_now = True
|
||||
matched_rid = only_rid
|
||||
if drop_now:
|
||||
app_logging.logger.info(
|
||||
"[APIPILOT-AUDIO-DROP] FRAME DROP bytes=%dB matchedResponse=%s (CALL marker in this response → leaked Qwen3 audio DISCARDED)",
|
||||
len(message),
|
||||
(matched_rid or session_ctx.get("_ws_current_response_id") or "-"),
|
||||
)
|
||||
continue
|
||||
else:
|
||||
app_logging.logger.debug(
|
||||
"[APIPILOT-AUDIO-ALLOW] FRAME PASS bytes=%dB responseCtx=%s dropIds=%s",
|
||||
len(message),
|
||||
(session_ctx.get("_ws_current_response_id") or "-"),
|
||||
(sorted(list(drop_ids.keys())) if isinstance(drop_ids, dict) else "none"),
|
||||
)
|
||||
# #region debug-point B:upstream-bytes-forward
|
||||
await realtime_events._report_server_debug(
|
||||
"B",
|
||||
@@ -623,535 +372,7 @@ async def _upstream_to_client(
|
||||
# #endregion
|
||||
await ws.send_bytes(message)
|
||||
else:
|
||||
# Extract the original message before sanitation removes pass-2 markers.
|
||||
pre_sanitize_summary = realtime_events._realtime_event_summary(message)
|
||||
pre_event_type = pre_sanitize_summary.get("type") if isinstance(pre_sanitize_summary, dict) else None
|
||||
pre_response_id: str | None = None
|
||||
if isinstance(pre_sanitize_summary, dict):
|
||||
if isinstance(pre_sanitize_summary.get("response_id"), str):
|
||||
pre_response_id = pre_sanitize_summary["response_id"]
|
||||
elif isinstance(pre_sanitize_summary.get("id"), str):
|
||||
pre_response_id = pre_sanitize_summary["id"]
|
||||
# Track the current response_id for the binary-audio gate.
|
||||
if isinstance(pre_response_id, str) and pre_response_id:
|
||||
session_ctx["_ws_current_response_id"] = pre_response_id
|
||||
# Detect CALL markers in every text event, including before response.done.
|
||||
# Mark this response_id to drop its audio and sanitize later events.
|
||||
if (
|
||||
isinstance(pre_sanitize_summary, dict)
|
||||
and isinstance(pre_event_type, str)
|
||||
and pre_event_type not in {"response.done"}
|
||||
):
|
||||
quick_text_parts: list[str] = []
|
||||
for _k in (
|
||||
"text",
|
||||
"transcript",
|
||||
"output_text",
|
||||
"audio_text",
|
||||
"output_transcript",
|
||||
"final_transcript",
|
||||
"output_audio_text",
|
||||
"content",
|
||||
"message",
|
||||
"utterance",
|
||||
"delta",
|
||||
"_raw_preview",
|
||||
):
|
||||
_v = pre_sanitize_summary.get(_k)
|
||||
if isinstance(_v, str) and _v.strip():
|
||||
quick_text_parts.append(_v)
|
||||
for _cname in ("item", "part", "output", "audio"):
|
||||
_sub = pre_sanitize_summary.get(_cname)
|
||||
if isinstance(_sub, dict):
|
||||
for _k2 in ("text", "transcript", "output_text"):
|
||||
_v2 = _sub.get(_k2)
|
||||
if isinstance(_v2, str) and _v2.strip():
|
||||
quick_text_parts.append(_v2)
|
||||
quick_text = (" ".join(quick_text_parts)).strip()
|
||||
if quick_text:
|
||||
_detected_name, _detected_args = services_tool_context._apipilot_parse_call_marker(quick_text)
|
||||
if isinstance(_detected_name, str) and _detected_name:
|
||||
drop_ids2 = session_ctx.get("_ws_drop_audio_for_response_ids")
|
||||
if not isinstance(drop_ids2, dict):
|
||||
drop_ids2 = {}
|
||||
session_ctx["_ws_drop_audio_for_response_ids"] = drop_ids2
|
||||
_flag_rid = (
|
||||
pre_response_id
|
||||
if isinstance(pre_response_id, str) and pre_response_id
|
||||
else f"auto-{id(session_ctx)}"
|
||||
)
|
||||
drop_ids2[_flag_rid] = True
|
||||
# Remove older IDs when more than ten remain.
|
||||
if len(drop_ids2) > 10:
|
||||
try:
|
||||
for _old_k in list(drop_ids2.keys())[:-10]:
|
||||
drop_ids2.pop(_old_k, None)
|
||||
except Exception:
|
||||
pass
|
||||
app_logging.logger.info(
|
||||
"[APIPILOT-SANITIZE] CALL marker in event=%s response=%s name=%s → AUDIO DROP GATE enabled (discard potential leaked Qwen3 audio)",
|
||||
pre_event_type,
|
||||
_flag_rid,
|
||||
_detected_name,
|
||||
)
|
||||
# 1a) Accumulate original TTS transcripts for pass-2 marker parsing.
|
||||
if (
|
||||
isinstance(pre_sanitize_summary, dict)
|
||||
and isinstance(pre_event_type, str)
|
||||
and pre_event_type in {"response.audio_transcript.done", "response.output_audio_transcript.done"}
|
||||
):
|
||||
pre_agent_text = (
|
||||
cast(str, pre_sanitize_summary.get("text"))
|
||||
if isinstance(pre_sanitize_summary.get("text"), str)
|
||||
else ""
|
||||
).strip()
|
||||
rid_key = (
|
||||
pre_response_id
|
||||
if isinstance(pre_response_id, str) and pre_response_id
|
||||
else f"auto-{id(session_ctx)}"
|
||||
)
|
||||
if rid_key and pre_agent_text:
|
||||
acc = session_ctx.get("_ws_apipilot_asst_transcript_acc")
|
||||
if not isinstance(acc, dict):
|
||||
acc = {}
|
||||
session_ctx["_ws_apipilot_asst_transcript_acc"] = acc
|
||||
prev = acc.get(rid_key, "")
|
||||
if prev:
|
||||
acc[rid_key] = f"{prev} {pre_agent_text}".strip()
|
||||
else:
|
||||
acc[rid_key] = pre_agent_text
|
||||
# 1b) Also accumulate assistant conversation.item.created text,
|
||||
# which can contain markers outside TTS transcripts.
|
||||
elif (
|
||||
isinstance(pre_sanitize_summary, dict)
|
||||
and isinstance(pre_event_type, str)
|
||||
and pre_event_type == "conversation.item.created"
|
||||
and pre_sanitize_summary.get("role") == "assistant"
|
||||
):
|
||||
pre_conv_text = (
|
||||
cast(str, pre_sanitize_summary.get("text"))
|
||||
if isinstance(pre_sanitize_summary.get("text"), str)
|
||||
else ""
|
||||
).strip()
|
||||
rid_key2 = (
|
||||
pre_response_id
|
||||
if isinstance(pre_response_id, str) and pre_response_id
|
||||
else f"auto-{id(session_ctx)}"
|
||||
)
|
||||
if rid_key2 and pre_conv_text:
|
||||
acc2 = session_ctx.get("_ws_apipilot_asst_transcript_acc")
|
||||
if not isinstance(acc2, dict):
|
||||
acc2 = {}
|
||||
session_ctx["_ws_apipilot_asst_transcript_acc"] = acc2
|
||||
prev2 = acc2.get(rid_key2, "")
|
||||
if prev2:
|
||||
acc2[rid_key2] = f"{prev2} {pre_conv_text}".strip()
|
||||
else:
|
||||
acc2[rid_key2] = pre_conv_text
|
||||
# 2) Parse the original marker at response.done to trigger pass 2.
|
||||
if (
|
||||
isinstance(pre_sanitize_summary, dict)
|
||||
and isinstance(pre_event_type, str)
|
||||
and pre_event_type == "response.done"
|
||||
):
|
||||
rid_val: str = (
|
||||
pre_response_id
|
||||
if isinstance(pre_response_id, str)
|
||||
else (
|
||||
cast(str, pre_sanitize_summary.get("id"))
|
||||
if isinstance(pre_sanitize_summary.get("id"), str)
|
||||
else f"auto-{id(session_ctx)}"
|
||||
)
|
||||
)
|
||||
full_text = ""
|
||||
acc3 = session_ctx.get("_ws_apipilot_asst_transcript_acc")
|
||||
if rid_val and isinstance(acc3, dict):
|
||||
full_text = (acc3.pop(rid_val, "") if isinstance(acc3, dict) else "") or ""
|
||||
# Use the sole remaining accumulator when an upstream changes
|
||||
# response_id between events.
|
||||
if (
|
||||
(not isinstance(full_text, str) or not full_text.strip())
|
||||
and isinstance(acc3, dict)
|
||||
and len(acc3) == 1
|
||||
):
|
||||
_only_key = next(iter(acc3.keys()))
|
||||
full_text = (acc3.pop(_only_key, "") if isinstance(acc3, dict) else "") or ""
|
||||
marker_name, marker_args = None, None
|
||||
if isinstance(full_text, str) and full_text.strip():
|
||||
_p1_defaults = (
|
||||
session_ctx.get("apipilot_defaults")
|
||||
if isinstance(session_ctx.get("apipilot_defaults"), dict)
|
||||
else {}
|
||||
)
|
||||
marker_name, marker_args = services_tool_context._apipilot_parse_call_marker(
|
||||
full_text, defaults=_p1_defaults
|
||||
)
|
||||
app_logging.logger.info(
|
||||
"[APIPILOT-CALL-NAME] USE=WS_RESPONSE_DONE_DETECTION markerFound=%s name=%r argsKeys=%s",
|
||||
("Y" if isinstance(marker_name, str) and marker_name else "N"),
|
||||
(marker_name or ""),
|
||||
list((marker_args or {}).keys()),
|
||||
)
|
||||
# Always log pass-2 marker detection diagnostics.
|
||||
try:
|
||||
_preview = full_text.replace("\n", " ")
|
||||
if len(_preview) > 500:
|
||||
_preview = _preview[:500] + "…"
|
||||
except Exception:
|
||||
_preview = str(full_text)[:500]
|
||||
app_logging.logger.info(
|
||||
"[APIPILOT] REASONING PASS-2 (WS realtime): response.done DIAGNOSTIC fullLen=%d markerFound=%s markerName=%s argsKeys=%s defaultsAutoFill=%s acc3Remaining=%d fullTextPreview=%r",
|
||||
len(full_text or ""),
|
||||
("Y" if isinstance(marker_name, str) and marker_name else "N"),
|
||||
(marker_name if isinstance(marker_name, str) and marker_name else ""),
|
||||
(list(marker_args.keys()) if isinstance(marker_args, dict) else []),
|
||||
(list(_p1_defaults.keys()) if isinstance(_p1_defaults, dict) and _p1_defaults else "none"),
|
||||
(len(acc3) if isinstance(acc3, dict) else -1),
|
||||
_preview,
|
||||
)
|
||||
elif isinstance(full_text, str):
|
||||
app_logging.logger.info(
|
||||
"[APIPILOT] REASONING PASS-2 (WS realtime): response.done DIAGNOSTIC fullLen=0 EMPTY markerFound=N response_id=%r acc3Remaining=%d",
|
||||
rid_val,
|
||||
(len(acc3) if isinstance(acc3, dict) else -1),
|
||||
)
|
||||
else:
|
||||
app_logging.logger.info(
|
||||
"[APIPILOT] REASONING PASS-2 (WS realtime): response.done DIAGNOSTIC fullText is not str type=%r acc3=%r",
|
||||
type(full_text).__name__,
|
||||
(list(acc3.keys()) if isinstance(acc3, dict) else "?"),
|
||||
)
|
||||
needs_pass2 = (
|
||||
isinstance(marker_name, str)
|
||||
and bool(marker_name)
|
||||
and not bool(session_ctx.get("_ws_apipilot_pass2_active"))
|
||||
and (int(session_ctx.get("_ws_apipilot_pass2_count") or 0) < 3)
|
||||
)
|
||||
if needs_pass2:
|
||||
# Clear active audio-drop gates before pass 2: its response
|
||||
# contains actual user data and must be played normally.
|
||||
drop_ids_clean = session_ctx.get("_ws_drop_audio_for_response_ids")
|
||||
if isinstance(drop_ids_clean, dict) and drop_ids_clean:
|
||||
drop_ids_clean.clear()
|
||||
app_logging.logger.info(
|
||||
"[APIPILOT-AUDIO-DROP] PASS-2 start: clear audio-drop gate → forward pass-2 briefing audio normally"
|
||||
)
|
||||
args = marker_args if isinstance(marker_args, dict) else {}
|
||||
# Fill missing arguments or replace fictional prompt example
|
||||
# values with actual defaults. When defaults are empty, use
|
||||
# session_ctx["inject_phone"] from the query string.
|
||||
_p1_defaults_use = (
|
||||
session_ctx.get("apipilot_defaults")
|
||||
if isinstance(session_ctx.get("apipilot_defaults"), dict)
|
||||
else {}
|
||||
)
|
||||
_fallback_inject_phone = (
|
||||
session_ctx.get("inject_phone") if isinstance(session_ctx.get("inject_phone"), str) else ""
|
||||
)
|
||||
# Known fictional example values appearing in prompts.
|
||||
_FAKE_PHONE_PLACEHOLDERS: set[str] = {
|
||||
"+56912345678", # Prompt example from rule 7.
|
||||
"56912345678", # Variant without +.
|
||||
"+5612345678",
|
||||
"+1234567890",
|
||||
"+12345678",
|
||||
"+56XXXXXXX",
|
||||
"+56xxxxxxx",
|
||||
"",
|
||||
}
|
||||
|
||||
def _is_phone_fake(v: str) -> bool:
|
||||
c = str(v or "").strip()
|
||||
if c in _FAKE_PHONE_PLACEHOLDERS: # noqa: B023
|
||||
return True
|
||||
if len(c) < 7:
|
||||
return True
|
||||
if c.endswith("00000"):
|
||||
return True
|
||||
if "123456" in c or "xxxxxx" in c.lower() or "xxxx" in c.lower():
|
||||
return True
|
||||
if c.startswith("+56") and len(c) <= 9:
|
||||
return True
|
||||
return False
|
||||
|
||||
# Apply defaults and fallback to all available keys.
|
||||
if isinstance(_p1_defaults_use, dict) and _p1_defaults_use:
|
||||
for k, default_v in _p1_defaults_use.items():
|
||||
if default_v in (None, "", []):
|
||||
continue
|
||||
if k not in args or args[k] in (None, "", []):
|
||||
args[k] = default_v
|
||||
elif k == "phone" and isinstance(args.get("phone"), str):
|
||||
if _is_phone_fake(str(args.get("phone"))):
|
||||
args[k] = default_v
|
||||
# Replace fictional phone values even when defaults lacks phone.
|
||||
if (
|
||||
isinstance(args.get("phone"), str)
|
||||
and _is_phone_fake(str(args.get("phone")))
|
||||
and isinstance(_fallback_inject_phone, str)
|
||||
and _fallback_inject_phone.strip()
|
||||
):
|
||||
args["phone"] = _fallback_inject_phone.strip()
|
||||
# Refresh _p1_defaults for subsequent logging.
|
||||
_p1_defaults = _p1_defaults_use
|
||||
session_ctx["_ws_apipilot_pass2_count"] = int(session_ctx.get("_ws_apipilot_pass2_count") or 0) + 1
|
||||
session_ctx["_ws_apipilot_pass2_active"] = True
|
||||
# Reuse exact discovery auth/demo values: AquaPortal binds
|
||||
# authorized tools to the x-demo-session used for GET /tools.
|
||||
_exec_auth = (
|
||||
session_ctx.get("apipilot_exec_auth")
|
||||
if isinstance(session_ctx.get("apipilot_exec_auth"), str)
|
||||
else None
|
||||
)
|
||||
_exec_demo = (
|
||||
session_ctx.get("apipilot_exec_demo")
|
||||
if isinstance(session_ctx.get("apipilot_exec_demo"), str)
|
||||
else None
|
||||
)
|
||||
# Compatibility fallback for missing values:
|
||||
if not isinstance(_exec_auth, str) or not _exec_auth.strip():
|
||||
login_tok = session_ctx.get("login_token")
|
||||
_exec_auth = f"Bearer {login_tok}" if isinstance(login_tok, str) and login_tok.strip() else None
|
||||
if not isinstance(_exec_demo, str) or not _exec_demo.strip():
|
||||
_exec_demo = f"ws-pass2-{client_ip}-{id(session_ctx)}"
|
||||
# The SUPER API KEY overrides even SERVICE_TOKEN. Force bearer
|
||||
# and demo-session values to match the discovery bypass identity.
|
||||
if settings.S2S_APIPILOT_API_KEY:
|
||||
_exec_auth = f"Bearer {settings.S2S_APIPILOT_API_KEY}"
|
||||
_exec_demo = settings.S2S_APIPILOT_API_KEY
|
||||
# Apply the service-token override only without a SUPER API KEY.
|
||||
elif settings.S2S_APIPILOT_SERVICE_TOKEN:
|
||||
_svc = settings.S2S_APIPILOT_SERVICE_TOKEN.strip()
|
||||
if _svc:
|
||||
_exec_auth = f"Bearer {_svc}" if not _svc.lower().startswith("bearer ") else _svc
|
||||
# For authenticated tools with an empty/short bearer, force
|
||||
# phone login again so AquaPortal receives a valid user token.
|
||||
# The length heuristic treats fewer than 80 characters as short.
|
||||
_phone_lfb = (
|
||||
session_ctx.get("inject_phone") if isinstance(session_ctx.get("inject_phone"), str) else ""
|
||||
) or (inject_phone if isinstance(inject_phone, str) else "") # type: ignore[name-defined] # noqa: F821 - preserve the legacy fallback failure.
|
||||
_phone_lfb = _phone_lfb.strip() if isinstance(_phone_lfb, str) else ""
|
||||
_current_bearer_token = ""
|
||||
if isinstance(_exec_auth, str) and _exec_auth.lower().startswith("bearer "):
|
||||
_current_bearer_token = _exec_auth[len("bearer ") :].strip()
|
||||
_is_jwt_like = (
|
||||
bool(_current_bearer_token)
|
||||
and len(_current_bearer_token) >= 120
|
||||
and _current_bearer_token.count(".") >= 2
|
||||
)
|
||||
# Resolve requires_auth from recent session/cache tools; default true.
|
||||
_tools_recent: list[dict] = (
|
||||
cast(list[dict], session_ctx.get("apipilot_tools_pass1"))
|
||||
if isinstance(session_ctx.get("apipilot_tools_pass1"), list)
|
||||
else []
|
||||
)
|
||||
if not _tools_recent:
|
||||
try:
|
||||
_cached = (
|
||||
providers_apipilot._APIPILOT_CACHE.get("tools")
|
||||
if providers_apipilot._APIPILOT_CACHE
|
||||
else None
|
||||
)
|
||||
if isinstance(_cached, tuple) and _cached and isinstance(_cached[0], list):
|
||||
_tools_recent = _cached[0]
|
||||
except Exception: # noqa: BLE001
|
||||
_tools_recent = []
|
||||
_mtool = next(
|
||||
(t for t in _tools_recent if isinstance(t, dict) and t.get("name") == marker_name), None
|
||||
)
|
||||
_requires_auth = True if (_mtool is None) else bool(_mtool.get("requires_auth"))
|
||||
if (
|
||||
(not settings.S2S_APIPILOT_API_KEY)
|
||||
and settings.S2S_LOGIN_BY_PHONE
|
||||
and _phone_lfb
|
||||
and _requires_auth
|
||||
and (not _is_jwt_like)
|
||||
):
|
||||
try:
|
||||
_l_tok, _l_obj = await providers_caller._login_by_phone(_phone_lfb)
|
||||
if isinstance(_l_tok, str) and _l_tok.strip():
|
||||
_exec_auth = f"Bearer {_l_tok.strip()}"
|
||||
_tok_len = len(_l_tok.strip())
|
||||
_is_new_jwt = _tok_len >= 120 and _l_tok.strip().count(".") >= 2
|
||||
app_logging.logger.info(
|
||||
"[APIPILOT] REASONING PASS-2 (WS realtime): FORCED PASS-2 LOGIN (tool requires_auth) phoneLen=%d token=%s%s",
|
||||
len(_phone_lfb),
|
||||
f"YES len={_tok_len}",
|
||||
(" JWT?" if _is_new_jwt else (" SHORT?" if _tok_len < 80 else " OK")),
|
||||
)
|
||||
else:
|
||||
app_logging.logger.info(
|
||||
"[APIPILOT] REASONING PASS-2 (WS realtime): FORCED PASS-2 LOGIN token EXTRACTION FAILED phoneLen=%d (tool requires_auth → likely 403)",
|
||||
len(_phone_lfb),
|
||||
)
|
||||
except Exception as _lfb_exc: # noqa: BLE001
|
||||
app_logging.logger.info(
|
||||
"[APIPILOT] REASONING PASS-2 (WS realtime): FORCED PASS-2 LOGIN failed phoneLen=%d err=%r",
|
||||
len(_phone_lfb),
|
||||
_lfb_exc,
|
||||
)
|
||||
bearer = _exec_auth
|
||||
demo_key = _exec_demo
|
||||
try:
|
||||
app_logging.logger.info(
|
||||
"[APIPILOT-CALL-NAME] USE=WS_REALTIME_PASS2_TRIGGER name=%r argsKeys=%s argsPhone=%r",
|
||||
marker_name,
|
||||
list(args.keys()) if args else [],
|
||||
(args.get("phone") if isinstance(args.get("phone"), str) else ""),
|
||||
)
|
||||
app_logging.logger.info(
|
||||
"[APIPILOT] REASONING PASS-2 (WS realtime): trigger marker name=%s argsKeys=%s argsPhone=%s auth=%s defaultsAutoFill=%s demoSessionKey=%.40s",
|
||||
marker_name,
|
||||
list(args.keys()) if args else [],
|
||||
(args.get("phone") if isinstance(args.get("phone"), str) else ""),
|
||||
"Y" if bearer else "anon",
|
||||
list(_p1_defaults.keys()) if isinstance(_p1_defaults, dict) and _p1_defaults else "none",
|
||||
demo_key if isinstance(demo_key, str) else "?",
|
||||
)
|
||||
t0 = providers_apipilot._monotonic_now()
|
||||
app_logging.logger.info(
|
||||
"[APIPILOT-CALL-NAME] USE=WS_REALTIME_PASS2_EXEC_HELPER name=%r → forward a _apipilot_execute_tool",
|
||||
marker_name,
|
||||
)
|
||||
env_exec = providers_apipilot._apipilot_execute_tool(
|
||||
auth_header=bearer,
|
||||
demo_session=demo_key,
|
||||
name=cast(str, marker_name),
|
||||
arguments=args,
|
||||
)
|
||||
if hasattr(asyncio, "create_task"):
|
||||
envelope = await asyncio.wait_for(env_exec, timeout=60.0)
|
||||
else:
|
||||
envelope = await env_exec
|
||||
t1 = providers_apipilot._monotonic_now() - t0
|
||||
ok = bool(isinstance(envelope, dict) and envelope.get("success"))
|
||||
data = envelope.get("data") if isinstance(envelope, dict) else None
|
||||
err_obj = (
|
||||
envelope.get("error")
|
||||
if isinstance(envelope, dict) and isinstance(envelope.get("error"), dict)
|
||||
else None
|
||||
)
|
||||
if isinstance(data, str):
|
||||
tool_text = data
|
||||
elif isinstance(data, (dict, list)):
|
||||
try:
|
||||
tool_text = json.dumps(data, indent=2, ensure_ascii=False)
|
||||
except Exception: # noqa: BLE001
|
||||
tool_text = str(data)
|
||||
else:
|
||||
tool_text = ""
|
||||
if not ok:
|
||||
code = (
|
||||
err_obj.get("code")
|
||||
if isinstance(err_obj, dict) and err_obj.get("code") is not None
|
||||
else (envelope.get("code") if isinstance(envelope, dict) else None)
|
||||
)
|
||||
msg = (
|
||||
err_obj.get("message")
|
||||
if isinstance(err_obj, dict) and isinstance(err_obj.get("message"), str)
|
||||
else ""
|
||||
)
|
||||
prefix = ""
|
||||
if code is not None or msg:
|
||||
prefix = f"[{code if code is not None else 'ERROR'}] {msg}".strip() + "\n"
|
||||
tool_text = f"{prefix}{tool_text}".strip()
|
||||
app_logging.logger.info(
|
||||
"[APIPILOT] REASONING PASS-2 (WS realtime): execute DONE name=%s success=%s len=%d t=%.0fms",
|
||||
marker_name,
|
||||
"ok" if ok else "error",
|
||||
len(tool_text),
|
||||
t1,
|
||||
)
|
||||
call_result_dict: dict[str, Any] = {
|
||||
"name": marker_name,
|
||||
"text": tool_text or "",
|
||||
"error": (not ok),
|
||||
}
|
||||
# Use the light internal sanitizer for pass 2. The user-output
|
||||
# sanitizer would remove the RESULT wrapper and prevent the
|
||||
# upstream model from receiving the briefing result.
|
||||
len_raw_tool_text = len(call_result_dict.get("text") or "")
|
||||
if isinstance(call_result_dict.get("text"), str):
|
||||
call_result_dict["text"] = services_tool_context._sanitize_pass2_internal_context(
|
||||
call_result_dict["text"]
|
||||
)
|
||||
len_sanitized_tool_text = len(call_result_dict.get("text") or "")
|
||||
blk_pass2 = (
|
||||
services_tool_context._resolve_apipilot_reasoning_context_block(
|
||||
None,
|
||||
call_result=call_result_dict,
|
||||
)
|
||||
or ""
|
||||
)
|
||||
if not blk_pass2.strip():
|
||||
blk_pass2 = (tool_text or "").strip()
|
||||
len_raw_wrapper = len(blk_pass2 or "")
|
||||
# Light sanitation preserves the pass-2 RESULT wrapper.
|
||||
if blk_pass2.strip():
|
||||
blk_pass2 = services_tool_context._sanitize_pass2_internal_context(blk_pass2)
|
||||
len_sanitized_wrapper = len(blk_pass2 or "")
|
||||
app_logging.logger.info(
|
||||
"[APIPILOT] REASONING PASS-2 (WS realtime): block SIZES len_raw_tool_text=%d len_sanitized_tool_text=%d len_raw_wrapper=%d len_sanitized_wrapper=%d success=%s",
|
||||
len_raw_tool_text,
|
||||
len_sanitized_tool_text,
|
||||
len_raw_wrapper,
|
||||
len_sanitized_wrapper,
|
||||
("ok" if ok else "error"),
|
||||
)
|
||||
if blk_pass2.strip():
|
||||
# Reset _ws_current_response_id before pass-2 injection so
|
||||
# valid briefing audio cannot match the previous CALL response.
|
||||
session_ctx["_ws_current_response_id"] = None
|
||||
app_logging.logger.info(
|
||||
"[APIPILOT] REASONING PASS-2 (WS realtime): inject PASS-2 block len=%d -> send conversation.item.create + response.create upstream",
|
||||
len(blk_pass2),
|
||||
)
|
||||
try:
|
||||
await upstream.send(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "conversation.item.create",
|
||||
"item": {
|
||||
"type": "message",
|
||||
"role": "user",
|
||||
"content": [{"type": "input_text", "text": blk_pass2}],
|
||||
},
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
)
|
||||
await upstream.send(json.dumps({"type": "response.create"}, ensure_ascii=False))
|
||||
app_logging.logger.info(
|
||||
"[APIPILOT] REASONING PASS-2 (WS realtime): injection OK. Awaiting model response (pass2_active=Y until next response.done)",
|
||||
)
|
||||
except Exception as exc2: # noqa: BLE001
|
||||
session_ctx["_ws_apipilot_pass2_active"] = False
|
||||
app_logging.logger.info(
|
||||
"[APIPILOT] REASONING PASS-2 (WS realtime): inject FAIL upstream send err=%r. pass2_active=N.",
|
||||
exc2,
|
||||
)
|
||||
else:
|
||||
session_ctx["_ws_apipilot_pass2_active"] = False
|
||||
app_logging.logger.info(
|
||||
"[APIPILOT] REASONING PASS-2 (WS realtime): EMPTY result block -> skip injection. pass2_active=N.",
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
session_ctx["_ws_apipilot_pass2_active"] = False
|
||||
app_logging.logger.info(
|
||||
"[APIPILOT] REASONING PASS-2 (WS realtime): ABORT marker=%s err=%r. pass2_active=N (safety).",
|
||||
marker_name,
|
||||
exc,
|
||||
)
|
||||
elif isinstance(session_ctx.get("_ws_apipilot_pass2_active"), bool) and bool(
|
||||
session_ctx.get("_ws_apipilot_pass2_active")
|
||||
):
|
||||
session_ctx["_ws_apipilot_pass2_active"] = False
|
||||
app_logging.logger.info(
|
||||
"[APIPILOT] REASONING PASS-2 (WS realtime): response.done PASS-2 FINALE -> pass2_active=N safety counter=%s",
|
||||
int(session_ctx.get("_ws_apipilot_pass2_count") or 0),
|
||||
)
|
||||
# Layer 7 post-processing: after marker handling, safely remove
|
||||
# internal markers before forwarding text to the client.
|
||||
# Remove internal context markers before forwarding text to the client.
|
||||
message, was_sanitized = realtime_events._sanitize_realtime_event_raw(message, session_ctx)
|
||||
# #region debug-point B:upstream-text-forward
|
||||
summary = realtime_events._realtime_event_summary(message)
|
||||
@@ -1184,8 +405,7 @@ async def _upstream_to_client(
|
||||
# Agent output, including all event variants.
|
||||
agent_text = ""
|
||||
if event_type in {"response.audio_transcript.done", "response.output_audio_transcript.done"}:
|
||||
# Pass-2 accumulation already retained the original text.
|
||||
# This sanitized agent_text is only for UI and logs.
|
||||
# Use the sanitized agent text for UI and logs.
|
||||
agent_text = (summary.get("text") or "").strip()
|
||||
if agent_text:
|
||||
app_logging.logger.info(
|
||||
|
||||
Reference in New Issue
Block a user