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:
@@ -1,11 +1,17 @@
|
||||
from fastapi import APIRouter, HTTPException, Depends
|
||||
from fastapi.responses import StreamingResponse
|
||||
from ..schemas.models import ProcessRequest_chat, ProcessResponse_chat, ProcessRequest_get, ProcessResponse_get, ProcessRequest_set, ProcessResponse_set, ProcessResponse_delete_session, ProcessRequest_delete_session
|
||||
from fastgpt_client import AsyncChatClient, aiter_stream_events
|
||||
from fastgpt_client.exceptions import (
|
||||
APIError, AuthenticationError, RateLimitError, ValidationError
|
||||
from fastgpt_client import AsyncChatClient
|
||||
from ..backends.chat import (
|
||||
ChatBackend,
|
||||
ChatBackendAPIError,
|
||||
ChatBackendAuthenticationError,
|
||||
ChatBackendRateLimitError,
|
||||
ChatInput,
|
||||
FormUpdate,
|
||||
TextDelta,
|
||||
)
|
||||
from ..core.fastgpt_client import get_fastgpt_client
|
||||
from ..core.fastgpt_client import get_chat_backend, get_fastgpt_client
|
||||
from ..core.config import Config
|
||||
from ..utils.text_chunker import SentenceTextChunker, SentenceTextChunkerConfig
|
||||
from loguru import logger
|
||||
@@ -14,7 +20,6 @@ import re
|
||||
import time
|
||||
|
||||
router = APIRouter()
|
||||
FORM_EXTRACT_MODULE_NAME = "文本内容提取事故信息"
|
||||
STATE_TAG_PATTERN = re.compile(r"<state>\s*(\d+)\s*</state>", flags=re.DOTALL)
|
||||
STATUS_CODE_MAP = {
|
||||
'0000': '结束通话',
|
||||
@@ -60,45 +65,6 @@ def extract_first_state_and_clean_content(text: str) -> tuple[str | None, str]:
|
||||
return match.group(1), STATE_TAG_PATTERN.sub("", text)
|
||||
|
||||
|
||||
def parse_json_value(value):
|
||||
"""Parse JSON string values when possible."""
|
||||
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_from_flow_nodes(nodes):
|
||||
"""Extract form update data from the configured FastGPT content-extract node."""
|
||||
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", "")
|
||||
if not form_update:
|
||||
return {}
|
||||
return parse_json_value(form_update)
|
||||
|
||||
return {}
|
||||
|
||||
|
||||
def format_set_info_input(payload: dict, include_input_info: bool) -> str:
|
||||
"""Build optional setInfo input for FastGPT helper calls."""
|
||||
if not include_input_info:
|
||||
@@ -156,13 +122,17 @@ def create_sse_event(event: str, data: dict) -> str:
|
||||
async def chat(
|
||||
request: ProcessRequest_chat,
|
||||
stream: bool = False,
|
||||
client: AsyncChatClient = Depends(get_fastgpt_client)
|
||||
backend: ChatBackend = Depends(get_chat_backend)
|
||||
):
|
||||
"""Handle chat completion request."""
|
||||
json_data = request.model_dump()
|
||||
need_form_update = json_data.get('needFormUpdate', False)
|
||||
use_text_chunk = json_data.get('useTextChunk', False)
|
||||
chat_variables = {'needFormUpdate': need_form_update}
|
||||
chat_input = ChatInput(
|
||||
session_id=json_data['sessionId'],
|
||||
text=json_data['text'],
|
||||
need_form_update=need_form_update,
|
||||
)
|
||||
request_started_at = time.perf_counter()
|
||||
logger.info(
|
||||
"Chat request received "
|
||||
@@ -193,16 +163,8 @@ async def chat(
|
||||
else None
|
||||
)
|
||||
try:
|
||||
# Use SDK's create_chat_completion with stream=True
|
||||
response = await client.create_chat_completion(
|
||||
messages=[{"role": "user", "content": json_data['text']}],
|
||||
chatId=json_data['sessionId'],
|
||||
stream=True,
|
||||
detail=True,
|
||||
variables=chat_variables
|
||||
)
|
||||
logger.info(
|
||||
"FastGPT stream response opened "
|
||||
"Chat backend stream opened "
|
||||
f"sessionId={json_data['sessionId']} "
|
||||
f"open_latency_ms={(time.perf_counter() - stream_started_at) * 1000:.1f}"
|
||||
)
|
||||
@@ -279,41 +241,34 @@ async def chat(
|
||||
state_filter_buffer = ""
|
||||
return cleaned
|
||||
|
||||
async for event in aiter_stream_events(response):
|
||||
async for event in backend.stream(chat_input):
|
||||
try:
|
||||
if not first_event_logged:
|
||||
first_event_logged = True
|
||||
logger.info(
|
||||
"FastGPT stream first event "
|
||||
f"sessionId={json_data['sessionId']} kind={event.kind} "
|
||||
"Chat backend stream first event "
|
||||
f"sessionId={json_data['sessionId']} "
|
||||
f"kind={type(event).__name__} "
|
||||
f"ttfb_ms={(time.perf_counter() - stream_started_at) * 1000:.1f}"
|
||||
)
|
||||
|
||||
if event.kind == "flowResponses" and not module_form_sent:
|
||||
form_update = extract_form_update_from_flow_nodes(event.data)
|
||||
if form_update:
|
||||
form_update_payload = form_update
|
||||
if isinstance(event, FormUpdate) and not module_form_sent:
|
||||
if event.data:
|
||||
form_update_payload = event.data
|
||||
logger.info(
|
||||
"FastGPT stream formUpdate extracted "
|
||||
"Chat backend stream formUpdate received "
|
||||
f"sessionId={json_data['sessionId']} "
|
||||
f"type={type(form_update).__name__} "
|
||||
f"formUpdate={form_update!r}"
|
||||
f"type={type(event.data).__name__} "
|
||||
f"formUpdate={event.data!r}"
|
||||
)
|
||||
yield flush_form_update(form_update)
|
||||
yield flush_form_update(event.data)
|
||||
module_form_sent = True
|
||||
continue
|
||||
|
||||
if event.kind not in {"answer", "fastAnswer", "data"}:
|
||||
if not isinstance(event, TextDelta):
|
||||
continue
|
||||
|
||||
data = event.data
|
||||
if not isinstance(data, dict):
|
||||
continue
|
||||
|
||||
try:
|
||||
delta_content = data['choices'][0]['delta'].get('content', '')
|
||||
except (KeyError, IndexError):
|
||||
delta_content = ''
|
||||
delta_content = event.text
|
||||
if not delta_content:
|
||||
continue
|
||||
|
||||
@@ -400,23 +355,14 @@ async def chat(
|
||||
return StreamingResponse(event_generator(), media_type="text/event-stream")
|
||||
|
||||
try:
|
||||
# Use SDK's create_chat_completion
|
||||
response = await client.create_chat_completion(
|
||||
messages=[{"role": "user", "content": json_data['text']}],
|
||||
chatId=json_data['sessionId'],
|
||||
stream=False,
|
||||
detail=True,
|
||||
variables=chat_variables
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
result = await backend.complete(chat_input)
|
||||
logger.info(
|
||||
"FastGPT non-stream response received "
|
||||
"Chat backend non-stream response received "
|
||||
f"sessionId={json_data['sessionId']} "
|
||||
f"latency_ms={(time.perf_counter() - request_started_at) * 1000:.1f}"
|
||||
)
|
||||
|
||||
except AuthenticationError as e:
|
||||
except ChatBackendAuthenticationError as e:
|
||||
logger.error(f"Authentication error: {e}")
|
||||
return ProcessResponse_chat(
|
||||
sessionId=json_data['sessionId'],
|
||||
@@ -427,7 +373,7 @@ async def chat(
|
||||
code="401",
|
||||
msg="认证失败"
|
||||
)
|
||||
except RateLimitError as e:
|
||||
except ChatBackendRateLimitError as e:
|
||||
logger.error(f"Rate limit error: {e}")
|
||||
return ProcessResponse_chat(
|
||||
sessionId=json_data['sessionId'],
|
||||
@@ -438,7 +384,7 @@ async def chat(
|
||||
code="429",
|
||||
msg="请求过于频繁,请稍后重试"
|
||||
)
|
||||
except APIError as e:
|
||||
except ChatBackendAPIError as e:
|
||||
logger.error(f"API error: {e}")
|
||||
return ProcessResponse_chat(
|
||||
sessionId=json_data['sessionId'],
|
||||
@@ -462,39 +408,10 @@ async def chat(
|
||||
)
|
||||
|
||||
try:
|
||||
# Extract content from FastGPT response
|
||||
content = data['choices'][0]['message']['content']
|
||||
logger.info(f"FastGPT服务返回信息content: {content}")
|
||||
content = result.content
|
||||
logger.info(f"Chat backend returned content: {content}")
|
||||
|
||||
finish_reason = data['choices'][0]['finish_reason']
|
||||
|
||||
# Extract state variables
|
||||
state = data.get('newVariables', {}).get('state', {})
|
||||
if isinstance(state, str):
|
||||
state = json.loads(state)
|
||||
|
||||
transfer_to_human = state.get("transfer_to_human", False)
|
||||
ywrysw = state.get("ywrysw", False)
|
||||
ywfjdc = state.get("ywfjdc", False)
|
||||
ywmtc = state.get("ywmtc", False)
|
||||
jdcsl = state.get("jdcsl", 0)
|
||||
accident_info_complete = state.get("accident_info_complete", False)
|
||||
user_is_ready = state.get("user_is_ready", False)
|
||||
if isinstance(user_is_ready, str):
|
||||
user_is_ready = user_is_ready.lower() == 'true'
|
||||
driver_info_complete = state.get("driver_info_complete", False)
|
||||
drivers_info_complete = state.get("drivers_info_complete", False)
|
||||
driver_info_check = state.get("drivers_info_check", False)
|
||||
drivers_info_check = state.get("drivers_info_check", False)
|
||||
|
||||
logger.debug(f"State variables: {data.get('newVariables', {})}")
|
||||
|
||||
# Parse content - sometimes content is a string, sometimes it is a list
|
||||
content_stage_code = None
|
||||
if isinstance(content, list):
|
||||
logger.debug("content是一个list")
|
||||
content = content[0]['text']['content']
|
||||
|
||||
if isinstance(content, str):
|
||||
logger.debug("content是一个str")
|
||||
content_stage_code, content = extract_first_state_and_clean_content(content)
|
||||
@@ -509,10 +426,12 @@ async def chat(
|
||||
logger.error(f"content既不是list也不是str, type: {type(content)}")
|
||||
raise ValueError("大模型回复不是list也不是str")
|
||||
|
||||
nextStageCode = content_stage_code or data['newVariables']['status_code']
|
||||
nextStageCode = content_stage_code or result.status_code
|
||||
if not nextStageCode:
|
||||
raise ValueError("大模型回复中缺少state")
|
||||
nextStageCode = normalize_stage_code(nextStageCode)
|
||||
nextStage = STATUS_CODE_MAP.get(nextStageCode, '')
|
||||
form_update = extract_form_update_from_flow_nodes(data.get("responseData", []))
|
||||
form_update = result.form_update
|
||||
logger.info(
|
||||
"Chat non-stream completed "
|
||||
f"sessionId={json_data['sessionId']} "
|
||||
|
||||
Reference in New Issue
Block a user