| """Base adaptor interface and shared models for messaging platform gateways.""" |
|
|
| from __future__ import annotations |
|
|
| import uuid |
| from abc import ABC, abstractmethod |
| from dataclasses import dataclass, field |
| from typing import TYPE_CHECKING, Any |
|
|
| from fastapi import Request, Response |
|
|
| if TYPE_CHECKING: |
| from agentic_rag.data.models import AgentOutput |
|
|
|
|
| |
| |
| |
|
|
| @dataclass |
| class PlatformMessage: |
| """Normalized inbound message from any messaging platform.""" |
|
|
| platform: str |
| sender_id: str |
| sender_name: str = "" |
| chat_id: str = "" |
| chat_type: str = "single" |
| text: str = "" |
| msg_type: str = "text" |
| raw_payload: dict[str, Any] = field(default_factory=dict) |
| reply_token: str = "" |
|
|
|
|
| @dataclass |
| class PlatformResponse: |
| """Normalized outbound response to be sent back to a platform.""" |
|
|
| content: str |
| msg_type: str = "text" |
| status_code: int = 200 |
| extra: dict[str, Any] = field(default_factory=dict) |
|
|
|
|
| |
| |
| |
|
|
| class BasePlatformAdaptor(ABC): |
| """Template-method adaptor for a messaging platform. |
| |
| Subclasses override the platform-specific steps; the pipeline |
| (verify β parse β route β format) is shared. |
| """ |
|
|
| platform_name: str = "" |
|
|
| def __init__(self, config: Any) -> None: |
| self.config = config |
|
|
| |
|
|
| async def process(self, request: Request) -> Response: |
| """Full pipeline: verify β parse β run agent β format β respond.""" |
| |
| if not await self.verify_request(request): |
| return Response(status_code=403, content="Signature verification failed") |
|
|
| |
| msg = await self.parse_message(request) |
|
|
| |
| if msg.msg_type not in ("text",): |
| return Response(status_code=200, content=self._empty_ack()) |
|
|
| if not msg.text.strip(): |
| return Response(status_code=200, content=self._empty_ack()) |
|
|
| |
| try: |
| from agentic_rag.config.settings import get_settings |
| response_mode = get_settings().gateway.response_mode |
| except Exception: |
| response_mode = "sync" |
|
|
| |
| if response_mode == "async": |
| |
| import asyncio |
| asyncio.create_task(self._process_async(msg)) |
| return Response(status_code=200, content=self._empty_ack()) |
|
|
| |
| output = await self._run_agent(msg) |
| presp = await self.format_response(output, msg) |
| return await self._build_http_response(presp) |
|
|
| |
|
|
| @abstractmethod |
| async def verify_request(self, request: Request) -> bool: |
| """Verify the incoming webhook signature/token.""" |
| ... |
|
|
| @abstractmethod |
| async def parse_message(self, request: Request) -> PlatformMessage: |
| """Parse the platform-specific payload into a PlatformMessage.""" |
| ... |
|
|
| @abstractmethod |
| async def format_response( |
| self, output: "AgentOutput", msg: PlatformMessage |
| ) -> PlatformResponse: |
| """Convert AgentOutput to a platform-compatible response.""" |
| ... |
|
|
| @abstractmethod |
| async def _build_http_response(self, presp: PlatformResponse) -> Response: |
| """Build the HTTP response object for this platform.""" |
| ... |
|
|
| @abstractmethod |
| async def push_message(self, msg: PlatformMessage, text: str) -> None: |
| """Send a message to the platform's push API (used in async mode).""" |
| ... |
|
|
| |
|
|
| async def _run_agent(self, msg: PlatformMessage) -> "AgentOutput": |
| """Route the message to the RAG agent engine and return the result.""" |
| from agentic_rag.services.llm.factory import get_llm |
| from agentic_rag.orchestration.l1_tools.registry import get_tool_registry |
| from agentic_rag.agent.router import AgentRouter |
| from agentic_rag.data.models import AgentInput |
|
|
| |
| sid = await self._resolve_session(msg) |
|
|
| llm = get_llm() |
| registry = get_tool_registry() |
|
|
| |
| from agentic_rag.orchestration.l1_tools.rag_tools import RAGSearchTool |
| try: |
| registry.get("rag_search") |
| except Exception: |
| registry.register(RAGSearchTool()) |
|
|
| router = AgentRouter(llm, registry) |
| engine = await router.route(query=msg.text) |
|
|
| input_data = AgentInput( |
| query=msg.text, |
| parameters={"session_id": sid, "platform": msg.platform}, |
| ) |
|
|
| return await engine.run(input_data, turn_id=uuid.uuid4().hex) |
|
|
| async def _resolve_session(self, msg: PlatformMessage) -> str: |
| """Map (platform, sender_id, chat_id) β internal session_id.""" |
| from agentic_rag.entrypoints.gateway.session import get_platform_session_map |
| session_map = get_platform_session_map() |
| return session_map.get_or_create( |
| platform=msg.platform, |
| user_id=msg.sender_id, |
| chat_id=msg.chat_id, |
| ) |
|
|
| async def _process_async(self, msg: PlatformMessage) -> None: |
| """Background: run agent and push result to platform.""" |
| try: |
| output = await self._run_agent(msg) |
| presp = await self.format_response(output, msg) |
| await self.push_message(msg, presp.content) |
| except Exception: |
| import sys |
| print(f" [Gateway/{self.platform_name}] β Async process failed", flush=True) |
| sys.stdout.flush() |
|
|
| def _empty_ack(self) -> str: |
| return "" |
|
|
| |
|
|
| def _chunk_text(self, text: str, max_len: int | None = None) -> list[str]: |
| """Split long text into platform-friendly chunks.""" |
| if max_len is None: |
| try: |
| from agentic_rag.config.settings import get_settings |
| max_len = get_settings().gateway.max_reply_length |
| except Exception: |
| max_len = 2000 |
| chunks = [] |
| while len(text) > max_len: |
| split_at = text.rfind("\n", 0, max_len) |
| if split_at < max_len // 2: |
| split_at = text.rfind("γ", 0, max_len) |
| if split_at < max_len // 2: |
| split_at = text.rfind(". ", 0, max_len) |
| if split_at < max_len // 2: |
| split_at = max_len |
| chunks.append(text[: split_at + 1]) |
| text = text[split_at + 1 :].lstrip() |
| if text.strip(): |
| chunks.append(text) |
| return chunks |
|
|