Refactor chat backend integration and introduce new backend-neutral architecture
- Replaced direct FastGPT client usage with a backend-neutral chat interface, allowing for improved flexibility and maintainability. - Introduced a new `ChatBackend` protocol and implemented `FastGPTBackend` to handle chat operations. - Updated the chat endpoint to utilize the new backend structure, enhancing the handling of chat requests and responses. - Added comprehensive tests to ensure compatibility and functionality of the new backend integration.
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
"""Backend adapters used by the public API layer."""
|
||||
|
||||
from .chat import (
|
||||
ChatBackend,
|
||||
ChatBackendAPIError,
|
||||
ChatBackendAuthenticationError,
|
||||
ChatBackendError,
|
||||
ChatBackendRateLimitError,
|
||||
ChatInput,
|
||||
ChatResult,
|
||||
FormUpdate,
|
||||
TextDelta,
|
||||
)
|
||||
from .fastgpt import FastGPTBackend
|
||||
|
||||
__all__ = [
|
||||
"ChatBackend",
|
||||
"ChatBackendAPIError",
|
||||
"ChatBackendAuthenticationError",
|
||||
"ChatBackendError",
|
||||
"ChatBackendRateLimitError",
|
||||
"ChatInput",
|
||||
"ChatResult",
|
||||
"FastGPTBackend",
|
||||
"FormUpdate",
|
||||
"TextDelta",
|
||||
]
|
||||
@@ -0,0 +1,72 @@
|
||||
"""Backend-neutral chat contract."""
|
||||
|
||||
from collections.abc import AsyncIterator
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Protocol
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ChatInput:
|
||||
"""Input shared by all chat backend implementations."""
|
||||
|
||||
session_id: str
|
||||
text: str
|
||||
need_form_update: bool = False
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TextDelta:
|
||||
"""A piece of raw model text.
|
||||
|
||||
The text may contain a partial ``<state>...</state>`` prefix. Parsing that
|
||||
public protocol remains the responsibility of the FastAPI layer.
|
||||
"""
|
||||
|
||||
text: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FormUpdate:
|
||||
"""A structured form update produced alongside model text."""
|
||||
|
||||
data: Any
|
||||
|
||||
|
||||
ChatStreamEvent = TextDelta | FormUpdate
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ChatResult:
|
||||
"""Backend-neutral result for a non-streaming chat request."""
|
||||
|
||||
content: str
|
||||
status_code: str | None = None
|
||||
form_update: Any = field(default_factory=dict)
|
||||
|
||||
|
||||
class ChatBackend(Protocol):
|
||||
"""Contract implemented by FastGPT today and LangGraph later."""
|
||||
|
||||
def stream(self, chat_input: ChatInput) -> AsyncIterator[ChatStreamEvent]:
|
||||
"""Stream raw text and structured side-channel events."""
|
||||
...
|
||||
|
||||
async def complete(self, chat_input: ChatInput) -> ChatResult:
|
||||
"""Return one complete backend-neutral chat result."""
|
||||
...
|
||||
|
||||
|
||||
class ChatBackendError(Exception):
|
||||
"""Base error raised by a chat backend adapter."""
|
||||
|
||||
|
||||
class ChatBackendAuthenticationError(ChatBackendError):
|
||||
"""The backend rejected its configured credentials."""
|
||||
|
||||
|
||||
class ChatBackendRateLimitError(ChatBackendError):
|
||||
"""The backend rejected the request because of rate limiting."""
|
||||
|
||||
|
||||
class ChatBackendAPIError(ChatBackendError):
|
||||
"""The backend API failed or returned an invalid response."""
|
||||
@@ -0,0 +1,148 @@
|
||||
"""FastGPT implementation of the backend-neutral chat contract."""
|
||||
|
||||
import json
|
||||
from collections.abc import AsyncIterator
|
||||
from typing import Any
|
||||
|
||||
from fastgpt_client import AsyncChatClient, aiter_stream_events
|
||||
from fastgpt_client.exceptions import APIError, AuthenticationError, RateLimitError
|
||||
|
||||
from .chat import (
|
||||
ChatBackendAPIError,
|
||||
ChatBackendAuthenticationError,
|
||||
ChatBackendRateLimitError,
|
||||
ChatInput,
|
||||
ChatResult,
|
||||
FormUpdate,
|
||||
TextDelta,
|
||||
)
|
||||
|
||||
|
||||
FORM_EXTRACT_MODULE_NAME = "文本内容提取事故信息"
|
||||
|
||||
|
||||
def _parse_json_value(value: Any) -> Any:
|
||||
parsed = value
|
||||
for _ in range(3):
|
||||
if not isinstance(parsed, str):
|
||||
return parsed
|
||||
parsed = parsed.strip()
|
||||
if not parsed:
|
||||
return {}
|
||||
try:
|
||||
parsed = json.loads(parsed)
|
||||
except json.JSONDecodeError:
|
||||
return parsed
|
||||
return parsed
|
||||
|
||||
|
||||
def _extract_form_update(nodes: Any) -> Any:
|
||||
if not isinstance(nodes, list):
|
||||
return {}
|
||||
|
||||
for node in nodes:
|
||||
if not isinstance(node, dict):
|
||||
continue
|
||||
if node.get("moduleName") != FORM_EXTRACT_MODULE_NAME:
|
||||
continue
|
||||
|
||||
extract_result = node.get("extractResult", {})
|
||||
if not isinstance(extract_result, dict):
|
||||
return {}
|
||||
|
||||
form_update = extract_result.get("formUpdate", "")
|
||||
return _parse_json_value(form_update) if form_update else {}
|
||||
|
||||
return {}
|
||||
|
||||
|
||||
def _extract_content(data: dict[str, Any]) -> str:
|
||||
try:
|
||||
content = data["choices"][0]["message"]["content"]
|
||||
except (KeyError, IndexError, TypeError) as exc:
|
||||
raise ChatBackendAPIError("FastGPT response is missing message content") from exc
|
||||
|
||||
if isinstance(content, list):
|
||||
try:
|
||||
content = content[0]["text"]["content"]
|
||||
except (KeyError, IndexError, TypeError) as exc:
|
||||
raise ChatBackendAPIError(
|
||||
"FastGPT response contains invalid list content"
|
||||
) from exc
|
||||
|
||||
if not isinstance(content, str):
|
||||
raise ChatBackendAPIError("FastGPT message content is not text")
|
||||
return content
|
||||
|
||||
|
||||
class FastGPTBackend:
|
||||
"""Translate FastGPT SDK calls and events into the neutral chat contract."""
|
||||
|
||||
def __init__(self, client: AsyncChatClient):
|
||||
self._client = client
|
||||
|
||||
async def stream(self, chat_input: ChatInput) -> AsyncIterator[TextDelta | FormUpdate]:
|
||||
try:
|
||||
response = await self._client.create_chat_completion(
|
||||
messages=[{"role": "user", "content": chat_input.text}],
|
||||
chatId=chat_input.session_id,
|
||||
stream=True,
|
||||
detail=True,
|
||||
variables={"needFormUpdate": chat_input.need_form_update},
|
||||
)
|
||||
|
||||
async for event in aiter_stream_events(response):
|
||||
if event.kind == "flowResponses":
|
||||
form_update = _extract_form_update(event.data)
|
||||
if form_update:
|
||||
yield FormUpdate(form_update)
|
||||
continue
|
||||
|
||||
if event.kind not in {"answer", "fastAnswer", "data"}:
|
||||
continue
|
||||
if not isinstance(event.data, dict):
|
||||
continue
|
||||
|
||||
try:
|
||||
content = event.data["choices"][0]["delta"].get("content", "")
|
||||
except (KeyError, IndexError, TypeError, AttributeError):
|
||||
content = ""
|
||||
if content:
|
||||
yield TextDelta(content)
|
||||
except AuthenticationError as exc:
|
||||
raise ChatBackendAuthenticationError(str(exc)) from exc
|
||||
except RateLimitError as exc:
|
||||
raise ChatBackendRateLimitError(str(exc)) from exc
|
||||
except APIError as exc:
|
||||
raise ChatBackendAPIError(str(exc)) from exc
|
||||
|
||||
async def complete(self, chat_input: ChatInput) -> ChatResult:
|
||||
try:
|
||||
response = await self._client.create_chat_completion(
|
||||
messages=[{"role": "user", "content": chat_input.text}],
|
||||
chatId=chat_input.session_id,
|
||||
stream=False,
|
||||
detail=True,
|
||||
variables={"needFormUpdate": chat_input.need_form_update},
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
except AuthenticationError as exc:
|
||||
raise ChatBackendAuthenticationError(str(exc)) from exc
|
||||
except RateLimitError as exc:
|
||||
raise ChatBackendRateLimitError(str(exc)) from exc
|
||||
except APIError as exc:
|
||||
raise ChatBackendAPIError(str(exc)) from exc
|
||||
|
||||
if not isinstance(data, dict):
|
||||
raise ChatBackendAPIError("FastGPT response body is not an object")
|
||||
|
||||
status_code = data.get("newVariables", {}).get("status_code")
|
||||
if status_code is not None:
|
||||
status_code = str(status_code)
|
||||
|
||||
return ChatResult(
|
||||
content=_extract_content(data),
|
||||
status_code=status_code,
|
||||
form_update=_extract_form_update(data.get("responseData", [])),
|
||||
)
|
||||
Reference in New Issue
Block a user