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:
@@ -11,11 +11,9 @@ from vocero_api.core import config as settings
|
||||
from vocero_api.core import logging as app_logging
|
||||
from vocero_api.models import requests as models_requests
|
||||
from vocero_api.models.responses import ReasoningResponse
|
||||
from vocero_api.providers import apipilot as providers_apipilot
|
||||
from vocero_api.providers import caller as providers_caller
|
||||
from vocero_api.services import context as services_context
|
||||
from vocero_api.services import mcp_reasoning
|
||||
from vocero_api.services import tool_context as services_tool_context
|
||||
|
||||
|
||||
def _openai_headers() -> dict[str, str]:
|
||||
@@ -182,45 +180,6 @@ async def reasoning(req: models_requests.ReasoningRequest, request: Request) ->
|
||||
app_logging.logger.info(
|
||||
"[INJECT-SUMMARY] ⏭️ REASONING: latest summary skipped (context empty / upstream returned empty summary / fetch failed.)"
|
||||
)
|
||||
# Load dynamic ApiPilot tools before building context, independently of
|
||||
# S2S_INJECT_PROFILE.
|
||||
needs_apipilot = bool(settings.S2S_APIPILOT_TOOLS_URL) or bool(settings.S2S_APIPILOT_TOOLS_EXECUTE_URL)
|
||||
apipilot_tools_pass1: list[dict] = []
|
||||
apipilot_auth_header: str | None = None
|
||||
if isinstance(login_token, str) and login_token.strip():
|
||||
tk = login_token.strip()
|
||||
if tk.lower().startswith("bearer "):
|
||||
apipilot_auth_header = tk
|
||||
else:
|
||||
apipilot_auth_header = f"Bearer {tk}"
|
||||
# S2S_LOGIN_BY_PHONE resolves an AquaPortal JWT, used for requires_auth=true
|
||||
# execution. S2S_APIPILOT_SERVICE_TOKEN overrides it when configured.
|
||||
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
|
||||
if needs_apipilot:
|
||||
try:
|
||||
apipilot_t0 = providers_apipilot._monotonic_now()
|
||||
apipilot_tools_pass1 = await providers_apipilot._apipilot_fetch_tools(
|
||||
auth_header=apipilot_auth_header,
|
||||
demo_session=None,
|
||||
force_refresh=False,
|
||||
)
|
||||
app_logging.logger.info(
|
||||
"[APIPILOT] REASONING PASS-1: tool list ready N=%d t=%dms auth=%s",
|
||||
len(apipilot_tools_pass1),
|
||||
int((providers_apipilot._monotonic_now() - apipilot_t0) * 1000),
|
||||
"Y" if apipilot_auth_header else "-",
|
||||
)
|
||||
except Exception as exc:
|
||||
app_logging.logger.warning("[APIPILOT] REASONING: tool-list fetch failed, ignoring: %s", exc)
|
||||
apipilot_tools_pass1 = []
|
||||
inject_apipilot_text_pass1 = services_tool_context._resolve_apipilot_reasoning_context_block(
|
||||
apipilot_tools_pass1,
|
||||
call_result=None,
|
||||
)
|
||||
|
||||
user_context_parts: list[str] = []
|
||||
# ─── USER FIX BUG 3: TODAY fixed context block → PREPEND to user_context too
|
||||
# (the same anti-hallucination block gets prefixed in the SYSTEM instructions
|
||||
@@ -242,8 +201,6 @@ async def reasoning(req: models_requests.ReasoningRequest, request: Request) ->
|
||||
user_context_parts.append(inject_reports_text)
|
||||
if inject_summary_text:
|
||||
user_context_parts.append(inject_summary_text)
|
||||
if inject_apipilot_text_pass1:
|
||||
user_context_parts.append(inject_apipilot_text_pass1)
|
||||
original_user_context = (req.user_context or "").strip()
|
||||
if original_user_context:
|
||||
user_context_parts.append(original_user_context)
|
||||
@@ -334,18 +291,6 @@ async def reasoning(req: models_requests.ReasoningRequest, request: Request) ->
|
||||
len(inject_profile_text),
|
||||
services_context._strip_internal_markers(inject_profile_text),
|
||||
)
|
||||
if settings.S2S_DEBUG_PROMPT and inject_apipilot_text_pass1.strip():
|
||||
app_logging.logger.info(
|
||||
"[DEBUG-APIPILOT][REASONING augment] FULL ApiPilot tools list block (len=%d):\n%s\n",
|
||||
len(inject_apipilot_text_pass1),
|
||||
services_context._strip_internal_markers(inject_apipilot_text_pass1),
|
||||
)
|
||||
if needs_apipilot and inject_apipilot_text_pass1.strip():
|
||||
app_logging.logger.info(
|
||||
"[INJECT-APIPILOT] REASONING PASS-1: inject tool list (len=%d, Ntools=%d) into instructions and user_context.",
|
||||
len(inject_apipilot_text_pass1),
|
||||
len(apipilot_tools_pass1),
|
||||
)
|
||||
effective_instructions = services_context._augment_instructions_with_profile(
|
||||
req.instructions or "",
|
||||
inject_profile_text,
|
||||
@@ -354,28 +299,8 @@ async def reasoning(req: models_requests.ReasoningRequest, request: Request) ->
|
||||
inject_episode_text,
|
||||
profile_obj=profile_obj,
|
||||
)
|
||||
if inject_apipilot_text_pass1.strip():
|
||||
effective_instructions = (
|
||||
effective_instructions + "\n\n" if effective_instructions else ""
|
||||
) + inject_apipilot_text_pass1
|
||||
else:
|
||||
effective_instructions = (req.instructions or "").strip()
|
||||
if inject_apipilot_text_pass1.strip():
|
||||
app_logging.logger.info(
|
||||
"[INJECT-APIPILOT] REASONING PASS-1: inject tool list (len=%d, Ntools=%d) into instructions (inject_profile=false).",
|
||||
len(inject_apipilot_text_pass1),
|
||||
len(apipilot_tools_pass1),
|
||||
)
|
||||
effective_instructions = (
|
||||
effective_instructions + "\n\n" if effective_instructions else ""
|
||||
) + inject_apipilot_text_pass1
|
||||
if settings.S2S_DEBUG_PROMPT and inject_apipilot_text_pass1.strip():
|
||||
app_logging.logger.info(
|
||||
"[DEBUG-APIPILOT][REASONING augment] FULL ApiPilot tools list block (len=%d):\n%s\n",
|
||||
len(inject_apipilot_text_pass1),
|
||||
services_context._strip_internal_markers(inject_apipilot_text_pass1),
|
||||
)
|
||||
|
||||
# Call the OpenAI-compatible Chat Completions provider.
|
||||
if not settings.OPENAI_API_KEY:
|
||||
raise HTTPException(status_code=503, detail="OPENAI_API_KEY no está configurado.")
|
||||
@@ -385,8 +310,6 @@ async def reasoning(req: models_requests.ReasoningRequest, request: Request) ->
|
||||
system_parts.append(instructions)
|
||||
if effective_user_context.strip():
|
||||
system_parts.append(f"User context:\n{effective_user_context.strip()}")
|
||||
if inject_apipilot_text_pass1:
|
||||
system_parts.append(inject_apipilot_text_pass1)
|
||||
if req.language.strip():
|
||||
system_parts.append(
|
||||
"Reply only with the final assistant answer text. "
|
||||
@@ -474,183 +397,4 @@ async def reasoning(req: models_requests.ReasoningRequest, request: Request) ->
|
||||
)
|
||||
raise HTTPException(status_code=502, detail="El servicio de razonamiento de OpenAI no devolvió una respuesta.")
|
||||
app_logging.logger.info("[Vocero REASONING] engine=OPENAI transcript=%s answer=%s", transcript[:300], answer[:500])
|
||||
# Capture the ApiPilot marker, execute its tool and run OpenAI pass 2.
|
||||
tool_name_p1_oai, tool_args_p1_oai = services_tool_context._apipilot_parse_call_marker(answer)
|
||||
if tool_name_p1_oai:
|
||||
app_logging.logger.info(
|
||||
"[APIPILOT-CALL-NAME] USE=OPENAI_PASS2_TRIGGER name=%r argsKeys=%s",
|
||||
tool_name_p1_oai,
|
||||
sorted(list((tool_args_p1_oai or {}).keys())),
|
||||
)
|
||||
app_logging.logger.info(
|
||||
"[APIPILOT] REASONING PASS-2 trigger OPENAI marker name=%s args=%s",
|
||||
tool_name_p1_oai,
|
||||
json.dumps(tool_args_p1_oai or {}, ensure_ascii=True)[:300],
|
||||
)
|
||||
app_logging.logger.info(
|
||||
"[APIPILOT-CALL-NAME] USE=OPENAI_PASS2_EXEC_HELPER name=%r → forward to _apipilot_execute_tool",
|
||||
tool_name_p1_oai,
|
||||
)
|
||||
tool_env_oai_p1 = await providers_apipilot._apipilot_execute_tool(
|
||||
auth_header=apipilot_auth_header,
|
||||
demo_session=None,
|
||||
name=tool_name_p1_oai,
|
||||
arguments=tool_args_p1_oai or {},
|
||||
)
|
||||
is_tool_err_oai = not bool(tool_env_oai_p1.get("success"))
|
||||
tool_data_oai = tool_env_oai_p1.get("data")
|
||||
if isinstance(tool_data_oai, str):
|
||||
tool_text_oai = tool_data_oai
|
||||
elif tool_data_oai is None:
|
||||
tool_text_oai = ""
|
||||
else:
|
||||
try:
|
||||
tool_text_oai = json.dumps(tool_data_oai, ensure_ascii=False, indent=2)
|
||||
except Exception:
|
||||
tool_text_oai = str(tool_data_oai)
|
||||
if is_tool_err_oai and isinstance(tool_env_oai_p1.get("error"), dict):
|
||||
err_oai = tool_env_oai_p1["error"]
|
||||
err_oai_head = f"[{err_oai.get('code')}] {err_oai.get('message')}"
|
||||
tool_text_oai = (err_oai_head + ("\n" + tool_text_oai if tool_text_oai.strip() else "")).strip()
|
||||
inject_apipilot_pass2_oai = services_tool_context._resolve_apipilot_reasoning_context_block(
|
||||
[],
|
||||
call_result={
|
||||
"name": tool_name_p1_oai,
|
||||
"text": tool_text_oai,
|
||||
"error": is_tool_err_oai,
|
||||
},
|
||||
)
|
||||
app_logging.logger.info(
|
||||
"[APIPILOT] REASONING PASS-2 OPENAI execute name=%s success=%s resultLen=%d → second chat/completions call",
|
||||
tool_name_p1_oai,
|
||||
str(not is_tool_err_oai).lower(),
|
||||
len(tool_text_oai),
|
||||
)
|
||||
# Build pass 2 with the same instructions/context structure, replacing
|
||||
# inject_apipilot_text_pass1 with the result block inject_apipilot_pass2_oai.
|
||||
user_ctx_oai_2: list[str] = []
|
||||
if voice_lock_block.strip():
|
||||
user_ctx_oai_2.append(voice_lock_block)
|
||||
if today_block.strip():
|
||||
user_ctx_oai_2.append(today_block)
|
||||
if inject_episode_text:
|
||||
user_ctx_oai_2.append(inject_episode_text)
|
||||
if inject_profile_text:
|
||||
user_ctx_oai_2.append(inject_profile_text)
|
||||
if inject_reports_text:
|
||||
user_ctx_oai_2.append(inject_reports_text)
|
||||
if inject_summary_text:
|
||||
user_ctx_oai_2.append(inject_summary_text)
|
||||
if inject_apipilot_pass2_oai:
|
||||
user_ctx_oai_2.append(inject_apipilot_pass2_oai)
|
||||
if original_user_context:
|
||||
user_ctx_oai_2.append(original_user_context)
|
||||
user_ctx_oai_2 = services_context._dedup_context_blocks_by_type(user_ctx_oai_2)
|
||||
stripped_oai_2: list[str] = []
|
||||
for p in user_ctx_oai_2:
|
||||
sp = services_context._strip_internal_markers(p)
|
||||
if sp:
|
||||
stripped_oai_2.append(sp)
|
||||
user_ctx_oai_2 = stripped_oai_2
|
||||
eff_user_ctx_oai_2 = "\n\n".join(user_ctx_oai_2)
|
||||
if settings.S2S_INJECT_PROFILE:
|
||||
eff_inst_oai_2 = services_context._augment_instructions_with_profile(
|
||||
req.instructions or "",
|
||||
inject_profile_text,
|
||||
inject_reports_text,
|
||||
inject_summary_text,
|
||||
inject_episode_text,
|
||||
profile_obj=profile_obj,
|
||||
)
|
||||
else:
|
||||
eff_inst_oai_2 = (req.instructions or "").strip()
|
||||
sys_parts_oai_2: list[str] = []
|
||||
instr_oai_2 = eff_inst_oai_2 or ((req.instructions or "").strip())
|
||||
if instr_oai_2:
|
||||
sys_parts_oai_2.append(instr_oai_2)
|
||||
if eff_user_ctx_oai_2.strip():
|
||||
sys_parts_oai_2.append(f"User context:\n{eff_user_ctx_oai_2.strip()}")
|
||||
if inject_apipilot_pass2_oai:
|
||||
sys_parts_oai_2.append(inject_apipilot_pass2_oai)
|
||||
if req.language.strip():
|
||||
sys_parts_oai_2.append(
|
||||
"Reply only with the final assistant answer text. "
|
||||
f"Keep the answer natural and concise in this language: {req.language.strip()}."
|
||||
)
|
||||
else:
|
||||
sys_parts_oai_2.append(
|
||||
"Reply only with the final assistant answer text. Keep the answer natural and concise."
|
||||
)
|
||||
system_msg_oai_2 = "\n\n".join(sys_parts_oai_2)
|
||||
body_oai_2 = {
|
||||
"model": settings.OPENAI_REASONING_MODEL,
|
||||
"messages": [
|
||||
{"role": "system", "content": system_msg_oai_2},
|
||||
{"role": "user", "content": transcript},
|
||||
],
|
||||
}
|
||||
if settings.S2S_DEBUG_PROMPT:
|
||||
app_logging.logger.info(
|
||||
"[DEBUG-PROMPT][REASONING → OPENAI PASS-2] FULL SYSTEM message (len=%d):\n%s\n",
|
||||
len(system_msg_oai_2),
|
||||
system_msg_oai_2,
|
||||
)
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=30.0) as http_oai2:
|
||||
resp_oai_2 = await http_oai2.post(
|
||||
f"{settings.OPENAI_BASE_URL}/chat/completions",
|
||||
headers=_openai_headers(),
|
||||
json=body_oai_2,
|
||||
)
|
||||
except httpx.RequestError as exc:
|
||||
app_logging.logger.warning("[EXT][OPENAI PASS-2] unreachable: %r", exc)
|
||||
raise HTTPException( # noqa: B904
|
||||
status_code=502,
|
||||
detail="No se puede conectar con el servicio de razonamiento de OpenAI (segunda pasada).",
|
||||
)
|
||||
if resp_oai_2.status_code != 200:
|
||||
app_logging.logger.warning(
|
||||
"[EXT][OPENAI PASS-2] failed %s: %s", resp_oai_2.status_code, resp_oai_2.text[:400]
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=502,
|
||||
detail=f"La segunda pasada del razonamiento de OpenAI falló ({resp_oai_2.status_code}).",
|
||||
)
|
||||
try:
|
||||
payload_oai_2 = resp_oai_2.json()
|
||||
except Exception:
|
||||
app_logging.logger.warning("[EXT][OPENAI PASS-2] invalid JSON: %s", resp_oai_2.text[:400])
|
||||
raise HTTPException( # noqa: B904
|
||||
status_code=502,
|
||||
detail="El servicio de razonamiento de OpenAI devolvió un JSON no válido (segunda pasada).",
|
||||
)
|
||||
final_oai_ans = ""
|
||||
choices_oai_2 = payload_oai_2.get("choices")
|
||||
if isinstance(choices_oai_2, list) and choices_oai_2:
|
||||
msg_oai_2 = choices_oai_2[0].get("message") if isinstance(choices_oai_2[0], dict) else None
|
||||
if isinstance(msg_oai_2, dict):
|
||||
content_oai_2 = msg_oai_2.get("content")
|
||||
if isinstance(content_oai_2, str):
|
||||
final_oai_ans = content_oai_2.strip()
|
||||
usage_oai_2 = payload_oai_2.get("usage")
|
||||
if not final_oai_ans:
|
||||
# Last-resort fallback: raw tool-result text.
|
||||
app_logging.logger.warning("[EXT][OPENAI PASS-2] empty answer, falling back to raw tool-result text")
|
||||
final_oai_ans = (tool_text_oai or answer).strip()
|
||||
final_out_oai: dict = {"answer": final_oai_ans}
|
||||
if usage_oai_2:
|
||||
final_out_oai["usage_pass2"] = usage_oai_2
|
||||
final_out_oai["apipilot_invoked"] = {
|
||||
"name": tool_name_p1_oai,
|
||||
"success": not is_tool_err_oai,
|
||||
"result_length": len(tool_text_oai),
|
||||
}
|
||||
app_logging.logger.info(
|
||||
"[Vocero REASONING] engine=OPENAI PASS-2 transcript=%s finalAnswer=%s apipilot=%s",
|
||||
transcript[:300],
|
||||
final_oai_ans[:500],
|
||||
json.dumps(final_out_oai["apipilot_invoked"], ensure_ascii=True),
|
||||
)
|
||||
return validated_json_response(final_out_oai, ReasoningResponse)
|
||||
# Without a marker, return the original pass-1 answer.
|
||||
return validated_json_response({"answer": answer}, ReasoningResponse)
|
||||
|
||||
Reference in New Issue
Block a user