refactor: rename API and frontend projects to Vocero

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.
This commit is contained in:
2026-10-06 19:26:55 -03:00
parent e0304b2bdb
commit abd03ed07d
105 changed files with 353 additions and 312 deletions
+461
View File
@@ -0,0 +1,461 @@
from __future__ import annotations
import json
import re
from typing import Any, cast
from vocero_api.core import logging as app_logging
from vocero_api.services import context as services_context
from vocero_api.services import tool_context as services_tool_context
async def _report_server_debug(hypothesis_id: str, location: str, msg: str, data: dict | None = None) -> None:
return
def _realtime_event_summary(raw: str) -> dict | None:
try:
payload = json.loads(raw)
except Exception:
return None
if not isinstance(payload, dict):
return None
event_type = payload.get("type")
if not isinstance(event_type, str):
return None
summary: dict[str, Any] = {"type": event_type}
if event_type == "session.update":
session = cast(dict[str, Any], payload.get("session")) if isinstance(payload.get("session"), dict) else {}
tools = cast(list[Any], session.get("tools")) if isinstance(session.get("tools"), list) else []
summary["toolCount"] = len(tools)
summary["toolNames"] = [tool.get("name") for tool in tools if isinstance(tool, dict)]
summary["toolChoice"] = session.get("tool_choice")
summary["instructionLength"] = len(session.get("instructions") or "")
elif event_type in {"response.function_call_arguments.done", "error", "session.updated"}:
for key in ("name", "call_id", "code", "message", "response_id"):
if key in payload:
summary[key] = payload.get(key)
# --- INPUT (user speech → STT transcript) ---
# Can arrive from upstream (server-side STT) — match either direction.
elif event_type == "conversation.item.input_audio_transcription.completed":
summary["transcript"] = payload.get("transcript", "")
summary["item_id"] = payload.get("item_id")
elif event_type == "conversation.item.created":
item = payload.get("item") if isinstance(payload.get("item"), dict) else None
if isinstance(item, dict):
summary["item_id"] = item.get("id")
summary["role"] = item.get("role")
summary["item_type"] = item.get("type")
summary["status"] = item.get("status")
content_parts = []
for c in item.get("content") or []:
if isinstance(c, dict):
if c.get("type") in {"input_text", "text"} and isinstance(c.get("text"), str):
content_parts.append(c["text"])
elif c.get("type") == "transcript" and isinstance(c.get("transcript"), str):
content_parts.append(c["transcript"])
if content_parts:
summary["text"] = " ".join(content_parts)
# --- OUTPUT (agent text/speech generation) ---
elif event_type == "response.output_item.done":
item = payload.get("item") if isinstance(payload.get("item"), dict) else None
if isinstance(item, dict):
summary["item_id"] = item.get("id")
summary["role"] = item.get("role")
summary["status"] = item.get("status")
content_parts = []
for c in item.get("content") or []:
if isinstance(c, dict):
if c.get("type") == "text" and isinstance(c.get("text"), str) and c["text"]:
content_parts.append(c["text"])
elif c.get("type") == "audio" and isinstance(c.get("transcript"), str) and c["transcript"]:
content_parts.append(c["transcript"])
if content_parts:
summary["text"] = " ".join(content_parts)
elif event_type == "response.content_part.done":
part = payload.get("part") if isinstance(payload.get("part"), dict) else None
if isinstance(part, dict):
summary["response_id"] = payload.get("response_id")
summary["item_id"] = payload.get("item_id")
summary["output_index"] = payload.get("output_index")
summary["content_index"] = payload.get("content_index")
if part.get("type") == "text":
summary["text"] = part.get("text", "")
elif part.get("type") == "audio":
if isinstance(part.get("transcript"), str) and part["transcript"]:
summary["text"] = part["transcript"]
# Standard OpenAI variant:
elif event_type == "response.audio_transcript.done":
summary["response_id"] = payload.get("response_id")
summary["item_id"] = payload.get("item_id")
summary["text"] = payload.get("transcript", "")
# Custom upstream variant observed in user logs:
elif event_type == "response.output_audio_transcript.done":
summary["response_id"] = payload.get("response_id")
summary["item_id"] = payload.get("item_id")
summary["output_index"] = payload.get("output_index")
# Try field names in order of likelihood
text = ""
for key in (
"transcript",
"text",
"output_text",
"audio_text",
"output_transcript",
"final_transcript",
"output_audio_text",
"content",
"message",
"utterance",
):
v = payload.get(key)
if isinstance(v, str) and v.strip():
text = v.strip()
break
# If text is still empty, search known nested objects.
if not text:
for container in ("item", "part", "content", "output", "delta", "audio"):
sub = payload.get(container)
if isinstance(sub, dict):
for key in ("transcript", "text", "output_text"):
v = sub.get(key)
if isinstance(v, str) and v.strip():
text = v.strip()
break
if text:
break
if not text:
# Last resort: the content[] array.
arr = payload.get("content")
if isinstance(arr, list):
parts = []
for c in arr:
if isinstance(c, dict):
for key in ("transcript", "text"):
v = c.get(key)
if isinstance(v, str) and v.strip():
parts.append(v.strip())
if parts:
text = " ".join(parts)
summary["text"] = text
# If no text was found, retain a raw preview to inspect the custom format.
if not text:
preview = raw[:800]
try:
pretty = json.dumps(payload, ensure_ascii=True)[:800]
except Exception:
pretty = preview
summary["_raw_preview"] = pretty
elif event_type == "response.function_call_arguments.done":
summary["arguments"] = payload.get("arguments")
# This event confirms strategy B: ai-server accepted native tools[].
_tool_name = payload.get("name") if isinstance(payload.get("name"), str) else ""
_call_id = payload.get("call_id") if isinstance(payload.get("call_id"), str) else ""
_args_json = cast(str, payload.get("arguments")) if isinstance(payload.get("arguments"), str) else "{}"
app_logging.logger.info(
"[TOOLCALL] ✓ UPSTREAM emitted response.function_call_arguments.done! "
"tool=%s callId=%s argsLen=%d argsPreview=%s",
_tool_name or "(empty)",
_call_id or "-",
len(_args_json),
_args_json[:200],
)
try:
_args_parsed = json.loads(_args_json or "{}")
app_logging.logger.info(
"[TOOLCALL] ✓ Tool call parsed argsKeys=%s",
sorted(_args_parsed.keys()) if isinstance(_args_parsed, dict) else "N/A",
)
except Exception:
pass
return summary
# Suppress noisy high-frequency events in detailed logs. Internal debug
# reporting still tracks them when enabled.
_NOISY_REALTIME_EVENT_TYPES = frozenset(
{
"response.output_audio.delta",
"response.audio.delta",
"conversation.item.input_audio_transcription.delta",
"response.text.delta",
"response.output_text.delta",
"rate_limits.updated",
}
)
# Suppress client-side high-frequency events such as input_audio_buffer.append.
_NOISY_CLIENT_EVENT_TYPES = frozenset(
{
"input_audio_buffer.append",
"input_audio_buffer.commit",
"input_audio_buffer.clear",
"input_audio_buffer.speech_started",
"input_audio_buffer.speech_stopped",
}
)
def _extract_profile_for_session(session_ctx: dict) -> dict:
"""Return the profile dict stored in session_ctx (phone auth result)."""
p = session_ctx.get("profile_obj") or session_ctx.get("user_profile") or {}
if isinstance(p, dict) and isinstance(p.get("user"), dict):
return cast(dict[str, Any], p["user"])
if isinstance(p, dict):
return p
return {}
def _sanitize_agent_output(text: Any, session_ctx: dict[str, Any] | None = None) -> str:
"""Server-side FINAL filter applied to ALL assistant output text.
Guarantees (belt-and-suspenders over the 6 prompt layers):
1. No [...][xxx] bracket markers of any kind.
2. No inline { ... } JSON / dict literals.
3. No [-]: <internal action name> pseudo-markers.
4. No name anglicization: Alessandro → restored to Alessandro.
5. Trim + collapse multiple spaces.
"""
s = str(text or "")
if not s:
return s
# (0) Remove "action line" pseudo-markers FIRST, BEFORE stripping all
# other brackets, because the pattern uses the literal "[-]" prefix
# which would get silently swallowed by step (1) otherwise. Match:
# [-]: checking_podcast_report (bracket literal then colon)
# []: action_name (empty bracket variant)
# [ - ] : do_stuff (spaces inside & around)
# Pattern is intentionally TIGHT: after the colon we only take up to 6
# underscore/hyphen words so we cannot accidentally eat the following
# real user speech (bug: greedy class with ,. and 200-char upper bound ate
# the entire "Claro, revisaré el reporte." clause on 2026-09-11).
s = re.sub(
r"\[[\s\-]{0,6}\]\s*:\s*[a-z0-9_\-]+(?:[ \t]+[a-z0-9_\-]+){0,5}",
" ",
s,
)
# Also catch dangling residual patterns if brackets got normalized to a
# single char already — handles cases where upstream writes the "colon"
# action marker without explicit brackets.
s = re.sub(r"(?<!\w):\s*checking_[A-Za-z0-9_\-]{1,120}", " ", s)
# (1) Remove ALL bracket-markers — [rightspeak], [endspeak], [thinking],
# [anything_else01]. We also remove stray [ / ] that don't form a
# complete pair (e.g. leftover "hello] there").
s = re.sub(r"\[[^\]\n]{0,80}\]", " ", s)
# (2) Remove inline dict / JSON literals. Pattern: { ... }, possibly
# split across a few \n (not greedy). Conservative: matches balanced
# pairs up to a reasonable length, otherwise fallback.
_max_iter = 5
for _ in range(_max_iter):
new_s = re.sub(r"\{[^{}]{0,1600}\}", " ", s)
if new_s == s:
break
s = new_s
# (3) Name anglicization. Iterate profile name's known variants →
# replace with original (case-insensitive match).
if session_ctx is not None and isinstance(session_ctx, dict):
profile = _extract_profile_for_session(session_ctx)
if profile:
full_name = str(profile.get("name") or "").strip()
if full_name:
# Split into parts and check each
parts = [p for p in re.split(r"[\s\-_]+", full_name) if p]
for part in parts:
variants = services_context._common_name_variants(part)
orig_cap = part[0].upper() + part[1:] if part else part
for variant in variants:
# Case-insensitive global replace → original case
s = re.sub(
r"\b" + re.escape(variant) + r"\b",
orig_cap,
s,
flags=re.IGNORECASE,
)
# Also try full-name variants (combined)
combined = full_name.replace(" ", "") # noqa: F841
# (5) Collapse whitespace. Re-punctuation cleanup: avoid normalizing
# legitimate terminal "?" / "!" into nothing; only collapse doubled
# or space-separated punctuation that resulted from earlier deletes.
s = re.sub(r"[ \t\u00a0]+", " ", s)
s = re.sub(r"\n{3,}", "\n\n", s)
# Interior punctuation: normalize spaces, but keep the char itself.
# Example: "hello , world" → "hello, world"
s = re.sub(r"\s+([,.;:])\s+", r"\1 ", s)
# Terminal / exclamation / interrogation: only fix spacing, never strip.
s = re.sub(r"\s+([!?])", r"\1", s)
s = re.sub(r"([!?])\s{2,}", r"\1 ", s)
# Strip leftover whitespace. Do NOT strip the legitimate terminal
# punctuation chars ?, !, ., because they are part of the real speech.
# Only strip repeated trailing commas / semicolons that appear because
# we deleted bracket/JSON fragments that separated clauses.
s = s.strip()
# Strip 2+ identical terminal punctuation chars (e.g. "hello.." → "hello.")
s = re.sub(r"([,.;:!?])\1{1,}$", r"\1", s)
# Strip terminal comma / semicolon / colon that now dangle (clause joins)
# but preserve ., ?, !.
s = re.sub(r"[,;:]$", "", s).strip()
return s
def _sanitize_realtime_event_raw(raw_message: str, session_ctx: dict[str, Any] | None = None) -> tuple[str, bool]:
"""If the raw upstream event contains assistant OUTPUT text, sanitize it
in-place and return (new_json_str, True) — otherwise return (raw, False).
Mirrors exactly the same traversal used by _realtime_event_summary so we
touch every field that would become summary["text"].
"""
import json as _json
if not isinstance(raw_message, str) or not raw_message.strip():
return raw_message, False
try:
payload = _json.loads(raw_message)
except Exception:
return raw_message, False
if not isinstance(payload, dict):
return raw_message, False
event_type = payload.get("type")
if not isinstance(event_type, str):
return raw_message, False
changed = False
def _sanitize_value(v: Any) -> Any:
nonlocal changed
if isinstance(v, str) and v.strip():
cleaned = _sanitize_agent_output(v, session_ctx)
# Layer 8 removes internal ApiPilot CALL markers before UI/TTS.
# The pass-2 accumulator retains the original transcript so
# execution can still be triggered by its marker.
cleaned = services_tool_context._sanitize_text_strip_apipilot_markers(cleaned)
if cleaned != v:
changed = True
return cleaned
return v
# --- Event dispatch (same shape as _realtime_event_summary) -------------
# Sanitize streaming deltas as well as .done events. Qwen3-TTS reads deltas
# before generating binary audio and could otherwise pronounce the marker.
if event_type in {"response.audio_transcript.done", "response.audio_transcript.delta"}:
if "transcript" in payload:
payload["transcript"] = _sanitize_value(payload["transcript"])
if "delta" in payload and isinstance(payload.get("delta"), str):
payload["delta"] = _sanitize_value(payload["delta"])
for container in ("item", "part", "content", "output", "audio"):
sub = payload.get(container)
if isinstance(sub, dict):
for key in ("transcript", "text", "delta"):
if key in sub and isinstance(sub.get(key), str):
sub[key] = _sanitize_value(sub[key])
elif event_type in {"response.output_audio_transcript.done", "response.output_audio_transcript.delta"}:
# Top-level keys
for key in (
"transcript",
"text",
"output_text",
"audio_text",
"output_transcript",
"final_transcript",
"output_audio_text",
"content",
"message",
"utterance",
"delta",
):
if key in payload and isinstance(payload.get(key), str):
payload[key] = _sanitize_value(payload[key])
# Sub-containers
for container in ("item", "part", "content", "output", "delta", "audio"):
sub = payload.get(container)
if isinstance(sub, dict):
for key in ("transcript", "text", "output_text", "delta"):
if key in sub and isinstance(sub.get(key), str):
sub[key] = _sanitize_value(sub[key])
# content[] array
if isinstance(sub.get("content"), list):
for c in sub["content"]:
if isinstance(c, dict):
for k2 in ("transcript", "text", "delta"):
if k2 in c and isinstance(c.get(k2), str):
c[k2] = _sanitize_value(c[k2])
if isinstance(payload.get("content"), list):
for c in payload["content"]:
if isinstance(c, dict):
for k2 in ("transcript", "text", "delta"):
if k2 in c and isinstance(c.get(k2), str):
c[k2] = _sanitize_value(c[k2])
elif event_type in {
"response.text.delta",
"response.text.done",
"response.output_text.delta",
"response.output_text.done",
}:
for key in ("text", "output_text", "delta", "final_text", "content"):
if key in payload and isinstance(payload.get(key), str):
payload[key] = _sanitize_value(payload[key])
for container in ("item", "part", "output", "content", "text"):
sub = payload.get(container)
if isinstance(sub, dict):
for key in ("text", "output_text", "delta"):
if key in sub and isinstance(sub.get(key), str):
sub[key] = _sanitize_value(sub[key])
if isinstance(sub.get("content"), list):
for c in sub["content"]:
if isinstance(c, dict):
for k2 in ("text", "transcript", "delta"):
if k2 in c and isinstance(c.get(k2), str):
c[k2] = _sanitize_value(c[k2])
if isinstance(payload.get("content"), list):
for c in payload["content"]:
if isinstance(c, dict):
for k2 in ("text", "transcript", "delta"):
if k2 in c and isinstance(c.get(k2), str):
c[k2] = _sanitize_value(c[k2])
elif event_type == "response.content_part.done":
part = payload.get("part")
if isinstance(part, dict):
if "text" in part and isinstance(part.get("text"), str):
part["text"] = _sanitize_value(part["text"])
if "transcript" in part and isinstance(part.get("transcript"), str):
part["transcript"] = _sanitize_value(part["transcript"])
elif event_type == "response.content_part.added":
part = payload.get("part")
if isinstance(part, dict):
if "text" in part and isinstance(part.get("text"), str):
part["text"] = _sanitize_value(part["text"])
if "transcript" in part and isinstance(part.get("transcript"), str):
part["transcript"] = _sanitize_value(part["transcript"])
elif event_type in {
"response.content_part.text.delta",
"response.output_content.text.delta",
"response.content.text.delta",
}:
for key in ("text", "delta"):
if key in payload and isinstance(payload.get(key), str):
payload[key] = _sanitize_value(payload[key])
elif event_type in {"response.output_item.done", "response.output_item.added"}:
item = payload.get("item")
if isinstance(item, dict):
for c in item.get("content") or []:
if isinstance(c, dict):
if "text" in c and isinstance(c["text"], str):
c["text"] = _sanitize_value(c["text"])
if "transcript" in c and isinstance(c["transcript"], str):
c["transcript"] = _sanitize_value(c["transcript"])
elif event_type == "conversation.item.created":
item = payload.get("item")
if isinstance(item, dict) and item.get("role") == "assistant":
for c in item.get("content") or []:
if isinstance(c, dict):
if c.get("type") in {"input_text", "text"} and isinstance(c.get("text"), str):
c["text"] = _sanitize_value(c["text"])
if c.get("type") == "transcript" and isinstance(c.get("transcript"), str):
c["transcript"] = _sanitize_value(c["transcript"])
if not changed:
return raw_message, False
try:
new_raw = _json.dumps(payload, ensure_ascii=False)
except Exception:
return raw_message, False
return new_raw, True