mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-02 19:55:18 +02:00
refactor(gateway): route inbound events through platform bundles
This commit is contained in:
parent
e953be5e99
commit
5048b0fd7c
3 changed files with 160 additions and 40 deletions
|
|
@ -11,10 +11,9 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.db import ExternalChatBinding, NewChatMessage
|
from app.db import ExternalChatBinding, NewChatMessage
|
||||||
from app.gateway.auth_invariant import assert_authorization_invariant
|
from app.gateway.auth_invariant import assert_authorization_invariant
|
||||||
from app.gateway.base.translator import GatewayStreamEvent
|
from app.gateway.base.translator import BaseStreamTranslator, GatewayStreamEvent
|
||||||
from app.gateway.bindings import get_or_create_thread_for_binding
|
from app.gateway.bindings import get_or_create_thread_for_binding
|
||||||
from app.gateway.hitl_filter import DEFAULT_HITL_TOOL_NAMES
|
from app.gateway.hitl_filter import DEFAULT_HITL_TOOL_NAMES
|
||||||
from app.gateway.telegram.translator import TelegramStreamTranslator
|
|
||||||
from app.gateway.thread_lock import acquire_thread_lock, release_thread_lock
|
from app.gateway.thread_lock import acquire_thread_lock, release_thread_lock
|
||||||
from app.observability.metrics import record_gateway_turn_latency
|
from app.observability.metrics import record_gateway_turn_latency
|
||||||
from app.tasks.chat.stream_new_chat import stream_new_chat
|
from app.tasks.chat.stream_new_chat import stream_new_chat
|
||||||
|
|
@ -58,7 +57,8 @@ async def call_agent_for_gateway(
|
||||||
session: AsyncSession,
|
session: AsyncSession,
|
||||||
binding: ExternalChatBinding,
|
binding: ExternalChatBinding,
|
||||||
user_text: str,
|
user_text: str,
|
||||||
translator: TelegramStreamTranslator,
|
translator: BaseStreamTranslator,
|
||||||
|
platform_label: str = "telegram",
|
||||||
request_id: str | None = None,
|
request_id: str | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
user = await assert_authorization_invariant(session, binding)
|
user = await assert_authorization_invariant(session, binding)
|
||||||
|
|
@ -92,10 +92,10 @@ async def call_agent_for_gateway(
|
||||||
NewChatMessage.thread_id == thread.id,
|
NewChatMessage.thread_id == thread.id,
|
||||||
NewChatMessage.source == "surfsense",
|
NewChatMessage.source == "surfsense",
|
||||||
)
|
)
|
||||||
.values(source="telegram")
|
.values(source=platform_label)
|
||||||
)
|
)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
record_gateway_turn_latency(0, platform="telegram")
|
record_gateway_turn_latency(0, platform=platform_label)
|
||||||
finally:
|
finally:
|
||||||
release_thread_lock(thread.id)
|
release_thread_lock(thread.id)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,26 +15,19 @@ from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
||||||
|
|
||||||
from app.config import config
|
from app.config import config
|
||||||
from app.db import (
|
from app.db import (
|
||||||
ExternalChatBindingState,
|
ExternalChatAccount,
|
||||||
ExternalChatBinding,
|
ExternalChatBinding,
|
||||||
|
ExternalChatBindingState,
|
||||||
ExternalChatEventStatus,
|
ExternalChatEventStatus,
|
||||||
ExternalChatInboundEvent,
|
ExternalChatInboundEvent,
|
||||||
ExternalChatPeerKind,
|
ExternalChatPeerKind,
|
||||||
ExternalChatAccount,
|
|
||||||
NewChatThread,
|
NewChatThread,
|
||||||
async_session_maker,
|
async_session_maker,
|
||||||
)
|
)
|
||||||
from app.gateway.accounts import account_token
|
|
||||||
from app.gateway.agent_invoke import call_agent_for_gateway
|
from app.gateway.agent_invoke import call_agent_for_gateway
|
||||||
|
from app.gateway.base.commands import command_name
|
||||||
from app.gateway.bindings import get_or_create_thread_for_binding
|
from app.gateway.bindings import get_or_create_thread_for_binding
|
||||||
from app.gateway.telegram.adapter import TelegramAdapter
|
from app.gateway.registry import resolve_platform_bundle
|
||||||
from app.gateway.telegram.commands import (
|
|
||||||
command_name,
|
|
||||||
handle_help_command,
|
|
||||||
handle_start_command,
|
|
||||||
send_unbound_onboarding,
|
|
||||||
)
|
|
||||||
from app.gateway.telegram.translator import TelegramStreamTranslator
|
|
||||||
from app.observability.metrics import record_gateway_inbox_processed
|
from app.observability.metrics import record_gateway_inbox_processed
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -150,14 +143,15 @@ async def _dispatch_inbound_event(
|
||||||
await session.commit()
|
await session.commit()
|
||||||
return
|
return
|
||||||
|
|
||||||
token = account_token(account)
|
try:
|
||||||
if not token:
|
bundle = resolve_platform_bundle(account)
|
||||||
|
except RuntimeError as exc:
|
||||||
event.status = ExternalChatEventStatus.FAILED
|
event.status = ExternalChatEventStatus.FAILED
|
||||||
event.last_error = "missing_telegram_token"
|
event.last_error = str(exc)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
return
|
return
|
||||||
|
|
||||||
adapter = TelegramAdapter(token)
|
adapter = bundle.adapter
|
||||||
parsed = adapter.parse_inbound(event.raw_payload or {})
|
parsed = adapter.parse_inbound(event.raw_payload or {})
|
||||||
if parsed.external_peer_id is None:
|
if parsed.external_peer_id is None:
|
||||||
event.status = ExternalChatEventStatus.IGNORED
|
event.status = ExternalChatEventStatus.IGNORED
|
||||||
|
|
@ -179,7 +173,8 @@ async def _dispatch_inbound_event(
|
||||||
binding = result.scalars().first()
|
binding = result.scalars().first()
|
||||||
|
|
||||||
if parsed.external_peer_kind != ExternalChatPeerKind.DIRECT.value:
|
if parsed.external_peer_kind != ExternalChatPeerKind.DIRECT.value:
|
||||||
await adapter.leave_chat(external_peer_id=parsed.external_peer_id)
|
if hasattr(adapter, "leave_chat"):
|
||||||
|
await adapter.leave_chat(external_peer_id=parsed.external_peer_id)
|
||||||
event.status = ExternalChatEventStatus.IGNORED
|
event.status = ExternalChatEventStatus.IGNORED
|
||||||
event.last_error = "group_rejected"
|
event.last_error = "group_rejected"
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
|
@ -187,7 +182,7 @@ async def _dispatch_inbound_event(
|
||||||
|
|
||||||
cmd = command_name(parsed.text)
|
cmd = command_name(parsed.text)
|
||||||
if cmd == "/start":
|
if cmd == "/start":
|
||||||
handled = await handle_start_command(
|
handled = await bundle.commands.handle_start_command(
|
||||||
session=session, adapter=adapter, event=parsed
|
session=session, adapter=adapter, event=parsed
|
||||||
)
|
)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
|
@ -195,23 +190,39 @@ async def _dispatch_inbound_event(
|
||||||
return
|
return
|
||||||
|
|
||||||
if binding is None:
|
if binding is None:
|
||||||
await send_unbound_onboarding(
|
if bundle.auto_bind_owner and account.owner_user_id and account.owner_search_space_id:
|
||||||
adapter=adapter,
|
binding = ExternalChatBinding(
|
||||||
event=parsed,
|
account_id=account.id,
|
||||||
dashboard_url=_dashboard_url(),
|
user_id=account.owner_user_id,
|
||||||
)
|
search_space_id=account.owner_search_space_id,
|
||||||
event.status = ExternalChatEventStatus.IGNORED
|
state=ExternalChatBindingState.BOUND,
|
||||||
event.last_error = "unbound_chat"
|
external_peer_id=parsed.external_peer_id,
|
||||||
await session.commit()
|
external_peer_kind=parsed.external_peer_kind,
|
||||||
return
|
external_display_name=parsed.display_name,
|
||||||
|
external_username=parsed.username,
|
||||||
|
external_metadata=parsed.metadata,
|
||||||
|
)
|
||||||
|
session.add(binding)
|
||||||
|
await session.flush()
|
||||||
|
else:
|
||||||
|
await bundle.commands.send_unbound_onboarding(
|
||||||
|
adapter=adapter,
|
||||||
|
event=parsed,
|
||||||
|
dashboard_url=_dashboard_url(),
|
||||||
|
)
|
||||||
|
event.status = ExternalChatEventStatus.IGNORED
|
||||||
|
event.last_error = "unbound_chat"
|
||||||
|
await session.commit()
|
||||||
|
return
|
||||||
|
|
||||||
event.external_chat_binding_id = binding.id
|
event.external_chat_binding_id = binding.id
|
||||||
|
|
||||||
if cmd == "/help":
|
if cmd == "/help":
|
||||||
await handle_help_command(adapter=adapter, event=parsed)
|
handled = await bundle.commands.handle_help_command(adapter=adapter, event=parsed)
|
||||||
event.status = ExternalChatEventStatus.PROCESSED
|
if handled:
|
||||||
await session.commit()
|
event.status = ExternalChatEventStatus.PROCESSED
|
||||||
return
|
await session.commit()
|
||||||
|
return
|
||||||
if cmd == "/new":
|
if cmd == "/new":
|
||||||
binding.new_chat_thread_id = None
|
binding.new_chat_thread_id = None
|
||||||
await adapter.send_message(
|
await adapter.send_message(
|
||||||
|
|
@ -231,21 +242,19 @@ async def _dispatch_inbound_event(
|
||||||
thread = await get_or_create_thread_for_binding(session, binding)
|
thread = await get_or_create_thread_for_binding(session, binding)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
|
||||||
translator = TelegramStreamTranslator(
|
translator = bundle.translator_factory(adapter, parsed)
|
||||||
adapter=adapter,
|
|
||||||
external_peer_id=parsed.external_peer_id,
|
|
||||||
)
|
|
||||||
await call_agent_for_gateway(
|
await call_agent_for_gateway(
|
||||||
session=session,
|
session=session,
|
||||||
binding=binding,
|
binding=binding,
|
||||||
user_text=parsed.text,
|
user_text=parsed.text,
|
||||||
translator=translator,
|
translator=translator,
|
||||||
|
platform_label=bundle.platform_label,
|
||||||
request_id=event.request_id or f"gateway:{inbox_id}",
|
request_id=event.request_id or f"gateway:{inbox_id}",
|
||||||
)
|
)
|
||||||
|
|
||||||
thread = await session.get(NewChatThread, thread.id)
|
thread = await session.get(NewChatThread, thread.id)
|
||||||
if thread is not None:
|
if thread is not None:
|
||||||
thread.source = "telegram"
|
thread.source = bundle.platform_label
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
111
surfsense_backend/app/gateway/registry.py
Normal file
111
surfsense_backend/app/gateway/registry.py
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
"""Resolve gateway platform implementations from account rows."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from app.db import ExternalChatAccount, ExternalChatAccountMode, ExternalChatPlatform
|
||||||
|
from app.gateway.accounts import account_token
|
||||||
|
from app.gateway.base.adapter import BasePlatformAdapter, ParsedInboundEvent
|
||||||
|
from app.gateway.base.commands import BaseGatewayCommands
|
||||||
|
from app.gateway.base.translator import BaseStreamTranslator
|
||||||
|
from app.gateway.telegram.adapter import TelegramAdapter
|
||||||
|
from app.gateway.telegram.commands import TelegramGatewayCommands
|
||||||
|
from app.gateway.telegram.translator import TelegramStreamTranslator
|
||||||
|
|
||||||
|
TranslatorFactory = Callable[
|
||||||
|
[BasePlatformAdapter, ParsedInboundEvent],
|
||||||
|
BaseStreamTranslator,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class PlatformBundle:
|
||||||
|
adapter: BasePlatformAdapter
|
||||||
|
translator_factory: TranslatorFactory
|
||||||
|
platform_label: str
|
||||||
|
commands: BaseGatewayCommands
|
||||||
|
auto_bind_owner: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
def _telegram_translator_factory(
|
||||||
|
adapter: BasePlatformAdapter,
|
||||||
|
event: ParsedInboundEvent,
|
||||||
|
) -> BaseStreamTranslator:
|
||||||
|
if event.external_peer_id is None:
|
||||||
|
raise RuntimeError("missing_external_peer_id")
|
||||||
|
return TelegramStreamTranslator(
|
||||||
|
adapter=adapter, # type: ignore[arg-type]
|
||||||
|
external_peer_id=event.external_peer_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _whatsapp_cloud_translator_factory(
|
||||||
|
adapter: BasePlatformAdapter,
|
||||||
|
event: ParsedInboundEvent,
|
||||||
|
) -> BaseStreamTranslator:
|
||||||
|
if event.external_peer_id is None:
|
||||||
|
raise RuntimeError("missing_external_peer_id")
|
||||||
|
from app.gateway.whatsapp.translator import WhatsAppCloudStreamTranslator
|
||||||
|
|
||||||
|
return WhatsAppCloudStreamTranslator(
|
||||||
|
adapter=adapter,
|
||||||
|
external_peer_id=event.external_peer_id,
|
||||||
|
inbound_message_id=event.external_message_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _whatsapp_baileys_translator_factory(
|
||||||
|
adapter: BasePlatformAdapter,
|
||||||
|
event: ParsedInboundEvent,
|
||||||
|
) -> BaseStreamTranslator:
|
||||||
|
if event.external_peer_id is None:
|
||||||
|
raise RuntimeError("missing_external_peer_id")
|
||||||
|
from app.gateway.whatsapp.translator_baileys import WhatsAppBaileysStreamTranslator
|
||||||
|
|
||||||
|
return WhatsAppBaileysStreamTranslator(
|
||||||
|
adapter=adapter,
|
||||||
|
external_peer_id=event.external_peer_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_platform_bundle(account: ExternalChatAccount) -> PlatformBundle:
|
||||||
|
if account.platform == ExternalChatPlatform.TELEGRAM:
|
||||||
|
token = account_token(account)
|
||||||
|
if not token:
|
||||||
|
raise RuntimeError("missing_telegram_token")
|
||||||
|
return PlatformBundle(
|
||||||
|
adapter=TelegramAdapter(token),
|
||||||
|
translator_factory=_telegram_translator_factory,
|
||||||
|
platform_label="telegram",
|
||||||
|
commands=TelegramGatewayCommands(),
|
||||||
|
)
|
||||||
|
|
||||||
|
if account.platform == ExternalChatPlatform.WHATSAPP:
|
||||||
|
if account.mode == ExternalChatAccountMode.CLOUD_SHARED:
|
||||||
|
from app.gateway.whatsapp.adapter_cloud import WhatsAppCloudAdapter
|
||||||
|
from app.gateway.whatsapp.commands import WhatsAppGatewayCommands
|
||||||
|
from app.gateway.whatsapp.credentials import (
|
||||||
|
load_system_whatsapp_credentials,
|
||||||
|
)
|
||||||
|
|
||||||
|
return PlatformBundle(
|
||||||
|
adapter=WhatsAppCloudAdapter(load_system_whatsapp_credentials()),
|
||||||
|
translator_factory=_whatsapp_cloud_translator_factory,
|
||||||
|
platform_label="whatsapp",
|
||||||
|
commands=WhatsAppGatewayCommands(),
|
||||||
|
auto_bind_owner=False,
|
||||||
|
)
|
||||||
|
if account.mode == ExternalChatAccountMode.SELF_HOST_BYO:
|
||||||
|
from app.gateway.whatsapp.adapter_baileys import WhatsAppBaileysAdapter
|
||||||
|
|
||||||
|
return PlatformBundle(
|
||||||
|
adapter=WhatsAppBaileysAdapter(),
|
||||||
|
translator_factory=_whatsapp_baileys_translator_factory,
|
||||||
|
platform_label="whatsapp",
|
||||||
|
commands=BaseGatewayCommands(),
|
||||||
|
auto_bind_owner=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
raise RuntimeError(f"unsupported_gateway_platform:{account.platform.value}:{account.mode.value}")
|
||||||
Loading…
Add table
Add a link
Reference in a new issue