Skip to content

Commit 40e2938

Browse files
committed
fix: route voice transcripts through agent bridge
1 parent c94e875 commit 40e2938

5 files changed

Lines changed: 512 additions & 160 deletions

File tree

app/agent_server.py

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
import mimetypes
1515
import json
16+
from collections.abc import Mapping
1617
mimetypes.add_type("application/javascript", ".js")
1718
import asyncio
1819
import uuid
@@ -1650,6 +1651,143 @@ async def _emit_agent_status_update(lanlan_name: Optional[str] = None) -> None:
16501651
pass
16511652

16521653

1654+
VOICE_TRANSCRIPT_CUSTOM_EVENT_TYPE = "voice_transcript"
1655+
VOICE_TRANSCRIPT_CUSTOM_EVENT_ID = "handle_transcript"
1656+
VOICE_TRANSCRIPT_CUSTOM_EVENT_TIMEOUT_SECONDS = 1.0
1657+
1658+
1659+
def _voice_bridge_noop(reason: str, **extra: object) -> Dict[str, Any]:
1660+
return {
1661+
"action": "noop",
1662+
"reason": str(reason or "noop"),
1663+
**extra,
1664+
}
1665+
1666+
1667+
def _voice_transcript_request_has_text(event: Mapping[str, object] | None) -> bool:
1668+
if not isinstance(event, Mapping):
1669+
return False
1670+
return bool(str(event.get("transcript") or "").strip())
1671+
1672+
1673+
def _voice_transcript_custom_event_args(event: Mapping[str, object]) -> Dict[str, object]:
1674+
metadata = event.get("metadata")
1675+
return {
1676+
"transcript": str(event.get("transcript") or "").strip(),
1677+
"lanlan_name": str(event.get("lanlan_name") or ""),
1678+
"metadata": dict(metadata) if isinstance(metadata, Mapping) else {},
1679+
}
1680+
1681+
1682+
def _voice_bridge_action_from_dispatch_results(dispatch_results: object) -> Dict[str, Any]:
1683+
if not isinstance(dispatch_results, list) or not dispatch_results:
1684+
return _voice_bridge_noop("no_subscribers")
1685+
1686+
from plugin.server.application.plugins.voice_contracts import (
1687+
arbitrate_voice_transcript_results,
1688+
)
1689+
1690+
arbitration_items: list[dict[str, object]] = []
1691+
failure_count = 0
1692+
for item in dispatch_results:
1693+
if not isinstance(item, Mapping):
1694+
continue
1695+
if not bool(item.get("success")):
1696+
failure_count += 1
1697+
continue
1698+
result = item.get("result")
1699+
if not isinstance(result, Mapping):
1700+
continue
1701+
action = str(result.get("action") or "").strip()
1702+
if not action:
1703+
continue
1704+
payload: Dict[str, Any] = dict(result)
1705+
payload["action"] = action
1706+
plugin_id = str(item.get("plugin_id") or "").strip()
1707+
if plugin_id:
1708+
payload.setdefault("source_plugin", plugin_id)
1709+
source_event_id = str(item.get("event_id") or "").strip()
1710+
if source_event_id:
1711+
payload.setdefault("source_event_id", source_event_id)
1712+
arbitration_items.append(
1713+
{
1714+
"plugin_id": payload.get("source_plugin") or plugin_id,
1715+
"event_id": payload.get("source_event_id") or source_event_id,
1716+
"success": True,
1717+
"result": payload,
1718+
}
1719+
)
1720+
1721+
if not arbitration_items:
1722+
return _voice_bridge_noop("no_handler_result", failures=failure_count)
1723+
payload = arbitrate_voice_transcript_results(arbitration_items)
1724+
if failure_count:
1725+
try:
1726+
existing_failures = int(payload.get("failures") or 0)
1727+
except (TypeError, ValueError):
1728+
existing_failures = 0
1729+
payload["failures"] = existing_failures + failure_count
1730+
return payload
1731+
1732+
1733+
async def _dispatch_voice_transcript_custom_event(
1734+
event: Mapping[str, object],
1735+
) -> Dict[str, Any]:
1736+
from plugin.server.application.plugins.dispatch_service import PluginDispatchService
1737+
1738+
dispatch_results = await PluginDispatchService().trigger_custom_event_subscribers(
1739+
event_type=VOICE_TRANSCRIPT_CUSTOM_EVENT_TYPE,
1740+
event_id=VOICE_TRANSCRIPT_CUSTOM_EVENT_ID,
1741+
args=_voice_transcript_custom_event_args(event),
1742+
timeout=VOICE_TRANSCRIPT_CUSTOM_EVENT_TIMEOUT_SECONDS,
1743+
)
1744+
return _voice_bridge_action_from_dispatch_results(dispatch_results)
1745+
1746+
1747+
async def _handle_voice_transcript_request(event: Dict[str, Any]) -> None:
1748+
event_id = str((event or {}).get("event_id") or "")
1749+
lanlan_name = (event or {}).get("lanlan_name")
1750+
result: Dict[str, Any] = _voice_bridge_noop("unavailable")
1751+
1752+
try:
1753+
if not _voice_transcript_request_has_text(event):
1754+
result = _voice_bridge_noop("empty_transcript")
1755+
elif not Modules.analyzer_enabled:
1756+
result = _voice_bridge_noop("agent_disabled")
1757+
elif not Modules.agent_flags.get("user_plugin_enabled", False):
1758+
result = _voice_bridge_noop("user_plugin_disabled")
1759+
else:
1760+
lifecycle_ready = bool(Modules.plugin_lifecycle_started)
1761+
if not lifecycle_ready:
1762+
lifecycle_ready = await _ensure_plugin_lifecycle_started()
1763+
1764+
if not lifecycle_ready:
1765+
result = _voice_bridge_noop("plugin_lifecycle_start_failed")
1766+
else:
1767+
result = await _dispatch_voice_transcript_custom_event(event)
1768+
except asyncio.CancelledError:
1769+
raise
1770+
except Exception as exc:
1771+
logger.debug(
1772+
"[VoiceBridge] plugin dispatch failed: event_id=%s lanlan=%s err=%s",
1773+
event_id,
1774+
lanlan_name,
1775+
exc,
1776+
)
1777+
result = {
1778+
"action": "noop",
1779+
"reason": "dispatch_failed",
1780+
"error_type": type(exc).__name__,
1781+
}
1782+
1783+
await _emit_main_event(
1784+
"voice_bridge_result",
1785+
lanlan_name if isinstance(lanlan_name, str) else None,
1786+
event_id=event_id,
1787+
result=result,
1788+
)
1789+
1790+
16531791
async def _on_session_event(event: Dict[str, Any]) -> None:
16541792
event_type = (event or {}).get("event_type")
16551793
if event_type == "agent_intent_restore_signal":
@@ -1662,6 +1800,20 @@ async def _on_session_event(event: Dict[str, Any]) -> None:
16621800
# has its own once-flag, so this is safe to spam.
16631801
await _maybe_restore_agent_intent()
16641802
return
1803+
if event_type == "voice_transcript_request":
1804+
seen_task = asyncio.create_task(
1805+
_emit_main_event(
1806+
"voice_bridge_request_seen",
1807+
event.get("lanlan_name") if isinstance(event.get("lanlan_name"), str) else None,
1808+
event_id=str(event.get("event_id") or ""),
1809+
)
1810+
)
1811+
Modules._background_tasks.add(seen_task)
1812+
seen_task.add_done_callback(Modules._background_tasks.discard)
1813+
task = asyncio.create_task(_handle_voice_transcript_request(event))
1814+
Modules._background_tasks.add(task)
1815+
task.add_done_callback(Modules._background_tasks.discard)
1816+
return
16651817
if event_type == "analyze_request":
16661818
messages = event.get("messages", [])
16671819
lanlan_name = event.get("lanlan_name")

app/main_server.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,13 @@ def _resolve_user_plugin_base() -> str:
121121
from fastapi.responses import JSONResponse, Response # noqa
122122
from fastapi.staticfiles import StaticFiles # noqa
123123
from main_logic import core as core, cross_server as cross_server # noqa
124-
from main_logic.agent_event_bus import MainServerAgentBridge, notify_analyze_ack, set_main_bridge # noqa
124+
from main_logic.agent_event_bus import (
125+
MainServerAgentBridge,
126+
notify_analyze_ack,
127+
notify_voice_bridge_request_seen,
128+
notify_voice_bridge_result,
129+
set_main_bridge,
130+
) # noqa
125131
from fastapi.templating import Jinja2Templates # noqa
126132
from dataclasses import dataclass # noqa
127133
from typing import Any, Optional # noqa
@@ -608,6 +614,17 @@ async def _handle_agent_event(event: dict):
608614
notify_analyze_ack(str(event.get("event_id") or ""))
609615
return
610616

617+
if event_type == "voice_bridge_result":
618+
notify_voice_bridge_result(
619+
str(event.get("event_id") or ""),
620+
event.get("result") if isinstance(event.get("result"), dict) else {},
621+
)
622+
return
623+
624+
if event_type == "voice_bridge_request_seen":
625+
notify_voice_bridge_request_seen(str(event.get("event_id") or ""))
626+
return
627+
611628
# Agent status updates may be broadcast (lanlan_name omitted).
612629
if event_type == "agent_status_update":
613630
payload = {

0 commit comments

Comments
 (0)