From be6af046ca19f26ba4e7b4153987a7f87a38d8d6 Mon Sep 17 00:00:00 2001 From: Xin Wang Date: Tue, 28 Jul 2026 10:38:01 +0800 Subject: [PATCH] Simplify client mode to single FastGPT app --- src/.env.example | 10 +--- src/api/endpoints.py | 32 +++++------ src/core/config.py | 38 +++---------- src/core/fastgpt_client.py | 82 ++++++++--------------------- src/schemas/models.py | 3 -- test/api/fastapi-browser-addon.http | 9 ++-- test/test_client_mode.py | 41 +++------------ 7 files changed, 55 insertions(+), 160 deletions(-) diff --git a/src/.env.example b/src/.env.example index 5ea0222..fa052e9 100644 --- a/src/.env.example +++ b/src/.env.example @@ -3,14 +3,8 @@ SECRET_KEY=your_secret_key DEBUG=True ANALYSIS_SERVICE_URL=http://127.0.0.1:3030 -ANALYSIS_AUTH_TOKEN=replace-with-direct-fastgpt-api-key -APP_ID=replace-with-direct-fastgpt-app-id - -# Browser Addon FastGPT application. It shares ANALYSIS_SERVICE_URL unless -# FASTGPT_BROWSER_ADDON_BASE_URL is set. -FASTGPT_BROWSER_ADDON_API_KEY=replace-with-browser-addon-fastgpt-api-key -FASTGPT_BROWSER_ADDON_APP_ID=replace-with-browser-addon-fastgpt-app-id -# FASTGPT_BROWSER_ADDON_BASE_URL=http://127.0.0.1:3030 +ANALYSIS_AUTH_TOKEN=replace-with-fastgpt-api-key +APP_ID=replace-with-fastgpt-app-id # Voice demo (Pipecat /ws-product). Relative to project root, or an absolute path. VOICE_CONFIG=config/voice.json diff --git a/src/api/endpoints.py b/src/api/endpoints.py index 833b190..77ab10f 100644 --- a/src/api/endpoints.py +++ b/src/api/endpoints.py @@ -1,11 +1,12 @@ -from fastapi import APIRouter, HTTPException +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 ..core.fastgpt_client import get_fastgpt_profile +from ..core.fastgpt_client import get_fastgpt_client +from ..core.config import Config from ..utils.text_chunker import SentenceTextChunker, SentenceTextChunkerConfig from loguru import logger import json @@ -113,14 +114,13 @@ def format_set_info_input(payload: dict, include_input_info: bool) -> str: async def delete_last_two_chat_records( client: AsyncChatClient, - app_id: str, session_id: str, ) -> None: """Delete the last two chat records.""" try: # Get chat records using SDK response = await client.get_chat_records( - appId=app_id, + appId=Config.FASTGPT_APP_ID, chatId=session_id, offset=0, pageSize=10 @@ -139,7 +139,7 @@ async def delete_last_two_chat_records( # Delete records using SDK for content_id in last_two_data_ids: delete_response = await client.delete_chat_record( - appId=app_id, + appId=Config.FASTGPT_APP_ID, chatId=session_id, contentId=content_id ) @@ -163,15 +163,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), ): """Handle chat completion request.""" json_data = request.model_dump() client_mode = json_data['clientMode'] - profile = get_fastgpt_profile(client_mode) - client = profile.client need_form_update = json_data.get('needFormUpdate', False) use_text_chunk = json_data.get('useTextChunk', False) - chat_variables = {'needFormUpdate': need_form_update} + chat_variables = { + 'needFormUpdate': need_form_update, + 'clientMode': client_mode, + } request_started_at = time.perf_counter() logger.info( "Chat request received " @@ -566,11 +568,10 @@ async def chat( @router.post("/set_info", response_model=ProcessResponse_set) async def set_info( request: ProcessRequest_set, + client: AsyncChatClient = Depends(get_fastgpt_client), ): """Set information in chat state.""" json_data = request.model_dump() - profile = get_fastgpt_profile(json_data['clientMode']) - client = profile.client set_info_payload = {'key': json_data['key'], 'value': json_data['value']} set_info_input = format_set_info_input( set_info_payload, @@ -605,7 +606,6 @@ async def set_info( try: await delete_last_two_chat_records( client, - profile.app_id, json_data['sessionId'], ) except Exception as e: @@ -638,7 +638,6 @@ async def set_info( # Delete records again after update await delete_last_two_chat_records( client, - profile.app_id, json_data['sessionId'], ) @@ -661,11 +660,10 @@ async def set_info( @router.post("/get_info", response_model=ProcessResponse_get) async def get_info( request: ProcessRequest_get, + client: AsyncChatClient = Depends(get_fastgpt_client), ): """Get information from chat state.""" json_data = request.model_dump() - profile = get_fastgpt_profile(json_data['clientMode']) - client = profile.client get_info_payload = {'key': json_data['key']} get_info_input = format_set_info_input( get_info_payload, @@ -701,7 +699,6 @@ async def get_info( try: await delete_last_two_chat_records( client, - profile.app_id, json_data['sessionId'], ) except Exception as e: @@ -768,11 +765,10 @@ async def get_info( @router.delete("/delete_session", response_model=ProcessResponse_delete_session) async def delete_session( request: ProcessRequest_delete_session, + client: AsyncChatClient = Depends(get_fastgpt_client), ): """Delete a chat session.""" json_data = request.model_dump() - profile = get_fastgpt_profile(json_data['clientMode']) - client = profile.client chat_id = json_data.get('sessionId') if not chat_id: @@ -786,7 +782,7 @@ async def delete_session( try: # Use SDK's delete_chat_history response = await client.delete_chat_history( - appId=profile.app_id, + appId=Config.FASTGPT_APP_ID, chatId=chat_id ) response.raise_for_status() diff --git a/src/core/config.py b/src/core/config.py index df70931..4bc2476 100644 --- a/src/core/config.py +++ b/src/core/config.py @@ -10,42 +10,18 @@ class Config: DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./test.db") DEBUG = os.getenv("DEBUG", "false").lower() in ("true", "1", "t") - # FastGPT Configuration. Direct mode falls back to the legacy environment - # variable names so existing deployments keep working unchanged. - FASTGPT_DIRECT_API_KEY = ( - os.getenv("FASTGPT_DIRECT_API_KEY") - or os.getenv("ANALYSIS_AUTH_TOKEN") - ) - FASTGPT_DIRECT_BASE_URL = ( - os.getenv("FASTGPT_DIRECT_BASE_URL") - or os.getenv("ANALYSIS_SERVICE_URL") - ) - FASTGPT_DIRECT_APP_ID = ( - os.getenv("FASTGPT_DIRECT_APP_ID") - or os.getenv("APP_ID") - ) - - FASTGPT_BROWSER_ADDON_API_KEY = os.getenv("FASTGPT_BROWSER_ADDON_API_KEY") - FASTGPT_BROWSER_ADDON_BASE_URL = ( - os.getenv("FASTGPT_BROWSER_ADDON_BASE_URL") - or FASTGPT_DIRECT_BASE_URL - ) - FASTGPT_BROWSER_ADDON_APP_ID = os.getenv("FASTGPT_BROWSER_ADDON_APP_ID") - - # Backward-compatible aliases for code outside the mode-aware API layer. - FASTGPT_API_KEY = FASTGPT_DIRECT_API_KEY - FASTGPT_BASE_URL = FASTGPT_DIRECT_BASE_URL - FASTGPT_APP_ID = FASTGPT_DIRECT_APP_ID + # FastGPT Configuration + FASTGPT_API_KEY = os.getenv("ANALYSIS_AUTH_TOKEN") + FASTGPT_BASE_URL = os.getenv("ANALYSIS_SERVICE_URL") + FASTGPT_APP_ID = os.getenv("APP_ID") @classmethod def validate(cls): """Validate required configuration.""" required = [ - ("FASTGPT_DIRECT_BASE_URL (or ANALYSIS_SERVICE_URL)", cls.FASTGPT_DIRECT_BASE_URL), - ("FASTGPT_DIRECT_API_KEY (or ANALYSIS_AUTH_TOKEN)", cls.FASTGPT_DIRECT_API_KEY), - ("FASTGPT_DIRECT_APP_ID (or APP_ID)", cls.FASTGPT_DIRECT_APP_ID), - ("FASTGPT_BROWSER_ADDON_API_KEY", cls.FASTGPT_BROWSER_ADDON_API_KEY), - ("FASTGPT_BROWSER_ADDON_APP_ID", cls.FASTGPT_BROWSER_ADDON_APP_ID), + ("FASTGPT_BASE_URL (ANALYSIS_SERVICE_URL)", cls.FASTGPT_BASE_URL), + ("FASTGPT_API_KEY (ANALYSIS_AUTH_TOKEN)", cls.FASTGPT_API_KEY), + ("FASTGPT_APP_ID (APP_ID)", cls.FASTGPT_APP_ID), ] missing = [name for name, value in required if not value] if missing: diff --git a/src/core/fastgpt_client.py b/src/core/fastgpt_client.py index ee6dfed..c0fd5bd 100644 --- a/src/core/fastgpt_client.py +++ b/src/core/fastgpt_client.py @@ -1,76 +1,38 @@ """FastGPT client dependency injection.""" -from contextlib import AsyncExitStack, asynccontextmanager -from dataclasses import dataclass +from contextlib import asynccontextmanager from fastapi import FastAPI from fastgpt_client import AsyncChatClient from .config import Config -@dataclass(frozen=True) -class FastGPTProfile: - """FastGPT client and app metadata for one client mode.""" - - client: AsyncChatClient - app_id: str - - -_fastgpt_profiles: dict[str, FastGPTProfile] = {} - - -def _profile_settings() -> dict[str, dict[str, str | None]]: - return { - "direct": { - "api_key": Config.FASTGPT_DIRECT_API_KEY, - "base_url": Config.FASTGPT_DIRECT_BASE_URL, - "app_id": Config.FASTGPT_DIRECT_APP_ID, - }, - "browser_addon": { - "api_key": Config.FASTGPT_BROWSER_ADDON_API_KEY, - "base_url": Config.FASTGPT_BROWSER_ADDON_BASE_URL, - "app_id": Config.FASTGPT_BROWSER_ADDON_APP_ID, - }, - } +_fastgpt_client: AsyncChatClient | None = None @asynccontextmanager async def lifespan(app: FastAPI): - """Manage all mode-specific FastGPT clients.""" + """Manage the FastGPT client lifecycle.""" + global _fastgpt_client Config.validate() - async with AsyncExitStack() as stack: - for client_mode, settings in _profile_settings().items(): - client = AsyncChatClient( - api_key=settings["api_key"], - base_url=settings["base_url"], - timeout=60.0, - max_retries=3, - retry_delay=1.0, - enable_logging=Config.DEBUG, - ) - await stack.enter_async_context(client) - _fastgpt_profiles[client_mode] = FastGPTProfile( - client=client, - app_id=settings["app_id"], - ) + _fastgpt_client = AsyncChatClient( + api_key=Config.FASTGPT_API_KEY, + base_url=Config.FASTGPT_BASE_URL, + timeout=60.0, + max_retries=3, + retry_delay=1.0, + enable_logging=Config.DEBUG, + ) + await _fastgpt_client.__aenter__() - try: - yield - finally: - _fastgpt_profiles.clear() - - -def get_fastgpt_profile(client_mode: str) -> FastGPTProfile: - """Get the initialized FastGPT profile for a client mode.""" try: - return _fastgpt_profiles[client_mode] - except KeyError as exc: - if client_mode not in _profile_settings(): - raise ValueError(f"Unsupported clientMode: {client_mode}") from exc - raise RuntimeError( - f"FastGPT profile is not initialized for clientMode={client_mode}" - ) from exc + yield + finally: + await _fastgpt_client.__aexit__(None, None, None) + _fastgpt_client = None -def get_fastgpt_client(client_mode: str = "direct") -> AsyncChatClient: - """Get a FastGPT client, defaulting to the legacy direct mode.""" - return get_fastgpt_profile(client_mode).client +def get_fastgpt_client() -> AsyncChatClient: + """Get the initialized FastGPT client.""" + if _fastgpt_client is None: + raise RuntimeError("FastGPT client not initialized") + return _fastgpt_client diff --git a/src/schemas/models.py b/src/schemas/models.py index 592c710..fdc004b 100644 --- a/src/schemas/models.py +++ b/src/schemas/models.py @@ -25,7 +25,6 @@ class ProcessRequest_get(BaseModel): sessionId: str = Field(..., max_length=64) timeStamp: str = Field(..., max_length=32) key: str = Field(...) - clientMode: ClientMode = "direct" includeInputInfo: bool = False class ProcessResponse_get(BaseModel): @@ -40,7 +39,6 @@ class ProcessRequest_set(BaseModel): timeStamp: str = Field(..., max_length=32) key: str = Field(...) value: str = Field(...) - clientMode: ClientMode = "direct" includeInputInfo: bool = False class ProcessResponse_set(BaseModel): @@ -52,7 +50,6 @@ class ProcessResponse_set(BaseModel): class ProcessRequest_delete_session(BaseModel): sessionId: str = Field(..., max_length=64) timeStamp: str = Field(..., max_length=32) - clientMode: ClientMode = "direct" class ProcessResponse_delete_session(BaseModel): sessionId: str = Field(..., max_length=64) diff --git a/test/api/fastapi-browser-addon.http b/test/api/fastapi-browser-addon.http index 932d780..ae6fc43 100644 --- a/test/api/fastapi-browser-addon.http +++ b/test/api/fastapi-browser-addon.http @@ -18,8 +18,7 @@ content-type: application/json { "sessionId": "{{sessionId}}", - "timeStamp": "{{$timestamp}}", - "clientMode": "{{clientMode}}" + "timeStamp": "{{$timestamp}}" } HTTP/1.1 200 - OK @@ -54,8 +53,7 @@ content-type: application/json { "sessionId": "{{sessionId}}", "timeStamp": "{{timeStamp}}", - "key": "hphm1", - "clientMode": "{{clientMode}}" + "key": "hphm1" } HTTP/1.1 200 - OK @@ -72,8 +70,7 @@ content-type: application/json "sessionId": "{{sessionId}}", "timeStamp": "{{timeStamp}}", "key": "hphm1", - "value": "沪A8939", - "clientMode": "{{clientMode}}" + "value": "沪A8939" } HTTP/1.1 200 - OK diff --git a/test/test_client_mode.py b/test/test_client_mode.py index a3a5dbb..a0764b9 100644 --- a/test/test_client_mode.py +++ b/test/test_client_mode.py @@ -2,42 +2,15 @@ import pytest from pydantic import ValidationError from src.api.endpoints import external_stage_code -from src.schemas.models import ( - ProcessRequest_chat, - ProcessRequest_delete_session, - ProcessRequest_get, - ProcessRequest_set, -) +from src.schemas.models import ProcessRequest_chat -@pytest.mark.parametrize( - ("request_model", "payload"), - [ - ( - ProcessRequest_chat, - {"sessionId": "session-1", "timeStamp": "1", "text": "你好"}, - ), - ( - ProcessRequest_set, - { - "sessionId": "session-1", - "timeStamp": "1", - "key": "name", - "value": "张三", - }, - ), - ( - ProcessRequest_get, - {"sessionId": "session-1", "timeStamp": "1", "key": "name"}, - ), - ( - ProcessRequest_delete_session, - {"sessionId": "session-1", "timeStamp": "1"}, - ), - ], -) -def test_client_mode_defaults_to_direct(request_model, payload): - request = request_model(**payload) +def test_client_mode_defaults_to_direct(): + request = ProcessRequest_chat( + sessionId="session-1", + timeStamp="1", + text="你好", + ) assert request.clientMode == "direct"