File size: 8,544 Bytes
6b62834 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 | """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
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Normalized Message / Response Models
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
@dataclass
class PlatformMessage:
"""Normalized inbound message from any messaging platform."""
platform: str # "wechat_work" | "dingtalk" | "feishu"
sender_id: str # Platform-specific user ID
sender_name: str = "" # Display name (optional)
chat_id: str = "" # Group chat ID or individual chat ID
chat_type: str = "single" # "single" | "group"
text: str = "" # Extracted text content
msg_type: str = "text" # "text" | "image" | "voice" | "event"
raw_payload: dict[str, Any] = field(default_factory=dict) # Original platform payload
reply_token: str = "" # Token/URL needed to send a reply
@dataclass
class PlatformResponse:
"""Normalized outbound response to be sent back to a platform."""
content: str # Formatted reply text
msg_type: str = "text" # "text" | "markdown" | "news" | "image"
status_code: int = 200
extra: dict[str, Any] = field(default_factory=dict) # Platform-specific extras (at_list, buttons, etc.)
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Base Adaptor (Template Method)
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
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
# ββ Template method ββββββββββββββββββββββββββββββββββββββ
async def process(self, request: Request) -> Response:
"""Full pipeline: verify β parse β run agent β format β respond."""
# 1. Verify
if not await self.verify_request(request):
return Response(status_code=403, content="Signature verification failed")
# 2. Parse
msg = await self.parse_message(request)
# Skip non-text messages gracefully
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())
# 3. Quick ACK if async mode
try:
from agentic_rag.config.settings import get_settings
response_mode = get_settings().gateway.response_mode
except Exception:
response_mode = "sync"
# 4. Run agent (may be long)
if response_mode == "async":
# Fire-and-forget: return 200 immediately, push result later
import asyncio
asyncio.create_task(self._process_async(msg))
return Response(status_code=200, content=self._empty_ack())
# 5. Sync: run agent inline and return result
output = await self._run_agent(msg)
presp = await self.format_response(output, msg)
return await self._build_http_response(presp)
# ββ Steps subclasses must implement ββββββββββββββββββββββ
@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)."""
...
# ββ Shared agent invocation ββββββββββββββββββββββββββββββ
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
# Resolve session
sid = await self._resolve_session(msg)
llm = get_llm()
registry = get_tool_registry()
# Register RAG search tool if not present
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 ""
# ββ Content helpers ββββββββββββββββββββββββββββββββββββββ
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
|