Simplify client mode to single FastGPT app
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user