Add client-mode FastGPT routing
This commit is contained in:
@@ -3,11 +3,14 @@ SECRET_KEY=your_secret_key
|
|||||||
DEBUG=True
|
DEBUG=True
|
||||||
|
|
||||||
ANALYSIS_SERVICE_URL=http://127.0.0.1:3030
|
ANALYSIS_SERVICE_URL=http://127.0.0.1:3030
|
||||||
ANALYSIS_AUTH_TOKEN=fastgpt-hSPnXMoBNGVAEpTLkQT3YfAnN26gQSyvLd4ABL1MRDoh68nL4RDlopFHXqmH8
|
ANALYSIS_AUTH_TOKEN=replace-with-direct-fastgpt-api-key
|
||||||
APP_ID=683ea1bc86197e19f71fc1ae
|
APP_ID=replace-with-direct-fastgpt-app-id
|
||||||
DELETE_SESSION_URL=http://127.0.0.1:3030/api/core/chat/delHistory?chatId={chatId}&appId={appId}
|
|
||||||
DELETE_CHAT_URL=http://127.0.0.1:3030/api/core/chat/item/delete?contentId={contentId}&chatId={chatId}&appId={appId}
|
# Browser Addon FastGPT application. It shares ANALYSIS_SERVICE_URL unless
|
||||||
GET_CHAT_RECORDS_URL=http://127.0.0.1:3030/api/core/chat/getPaginationRecords
|
# 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
|
||||||
|
|
||||||
# Voice demo (Pipecat /ws-product). Relative to project root, or an absolute path.
|
# Voice demo (Pipecat /ws-product). Relative to project root, or an absolute path.
|
||||||
VOICE_CONFIG=config/voice.json
|
VOICE_CONFIG=config/voice.json
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
from fastapi import APIRouter, HTTPException, Depends
|
from fastapi import APIRouter, HTTPException
|
||||||
from fastapi.responses import StreamingResponse
|
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 ..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 import AsyncChatClient, aiter_stream_events
|
||||||
from fastgpt_client.exceptions import (
|
from fastgpt_client.exceptions import (
|
||||||
APIError, AuthenticationError, RateLimitError, ValidationError
|
APIError, AuthenticationError, RateLimitError, ValidationError
|
||||||
)
|
)
|
||||||
from ..core.fastgpt_client import get_fastgpt_client
|
from ..core.fastgpt_client import get_fastgpt_profile
|
||||||
from ..core.config import Config
|
|
||||||
from ..utils.text_chunker import SentenceTextChunker, SentenceTextChunkerConfig
|
from ..utils.text_chunker import SentenceTextChunker, SentenceTextChunkerConfig
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
import json
|
import json
|
||||||
@@ -52,6 +51,13 @@ def normalize_stage_code(stage_code: str) -> str:
|
|||||||
return stage_code
|
return stage_code
|
||||||
|
|
||||||
|
|
||||||
|
def external_stage_code(client_mode: str, stage_code: str) -> str:
|
||||||
|
"""Apply the external namespace required by the client mode."""
|
||||||
|
if client_mode == "browser_addon":
|
||||||
|
return f"browser_addon.{stage_code}"
|
||||||
|
return stage_code
|
||||||
|
|
||||||
|
|
||||||
def extract_first_state_and_clean_content(text: str) -> tuple[str | None, str]:
|
def extract_first_state_and_clean_content(text: str) -> tuple[str | None, str]:
|
||||||
"""Return the first state code and content with all state tags removed."""
|
"""Return the first state code and content with all state tags removed."""
|
||||||
match = STATE_TAG_PATTERN.search(text)
|
match = STATE_TAG_PATTERN.search(text)
|
||||||
@@ -107,13 +113,14 @@ def format_set_info_input(payload: dict, include_input_info: bool) -> str:
|
|||||||
|
|
||||||
async def delete_last_two_chat_records(
|
async def delete_last_two_chat_records(
|
||||||
client: AsyncChatClient,
|
client: AsyncChatClient,
|
||||||
session_id: str
|
app_id: str,
|
||||||
|
session_id: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Delete the last two chat records."""
|
"""Delete the last two chat records."""
|
||||||
try:
|
try:
|
||||||
# Get chat records using SDK
|
# Get chat records using SDK
|
||||||
response = await client.get_chat_records(
|
response = await client.get_chat_records(
|
||||||
appId=Config.FASTGPT_APP_ID,
|
appId=app_id,
|
||||||
chatId=session_id,
|
chatId=session_id,
|
||||||
offset=0,
|
offset=0,
|
||||||
pageSize=10
|
pageSize=10
|
||||||
@@ -132,7 +139,7 @@ async def delete_last_two_chat_records(
|
|||||||
# Delete records using SDK
|
# Delete records using SDK
|
||||||
for content_id in last_two_data_ids:
|
for content_id in last_two_data_ids:
|
||||||
delete_response = await client.delete_chat_record(
|
delete_response = await client.delete_chat_record(
|
||||||
appId=Config.FASTGPT_APP_ID,
|
appId=app_id,
|
||||||
chatId=session_id,
|
chatId=session_id,
|
||||||
contentId=content_id
|
contentId=content_id
|
||||||
)
|
)
|
||||||
@@ -156,17 +163,19 @@ def create_sse_event(event: str, data: dict) -> str:
|
|||||||
async def chat(
|
async def chat(
|
||||||
request: ProcessRequest_chat,
|
request: ProcessRequest_chat,
|
||||||
stream: bool = False,
|
stream: bool = False,
|
||||||
client: AsyncChatClient = Depends(get_fastgpt_client)
|
|
||||||
):
|
):
|
||||||
"""Handle chat completion request."""
|
"""Handle chat completion request."""
|
||||||
json_data = request.model_dump()
|
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)
|
need_form_update = json_data.get('needFormUpdate', False)
|
||||||
use_text_chunk = json_data.get('useTextChunk', False)
|
use_text_chunk = json_data.get('useTextChunk', False)
|
||||||
chat_variables = {'needFormUpdate': need_form_update}
|
chat_variables = {'needFormUpdate': need_form_update}
|
||||||
request_started_at = time.perf_counter()
|
request_started_at = time.perf_counter()
|
||||||
logger.info(
|
logger.info(
|
||||||
"Chat request received "
|
"Chat request received "
|
||||||
f"sessionId={json_data['sessionId']} stream={stream} "
|
f"sessionId={json_data['sessionId']} clientMode={client_mode} stream={stream} "
|
||||||
f"needFormUpdate={need_form_update} useTextChunk={use_text_chunk} "
|
f"needFormUpdate={need_form_update} useTextChunk={use_text_chunk} "
|
||||||
f"text_len={len(json_data.get('text', ''))} "
|
f"text_len={len(json_data.get('text', ''))} "
|
||||||
f"input={json_data.get('text', '')!r}"
|
f"input={json_data.get('text', '')!r}"
|
||||||
@@ -325,11 +334,17 @@ async def chat(
|
|||||||
if state_code:
|
if state_code:
|
||||||
|
|
||||||
# Apply logic to map/adjust state code
|
# Apply logic to map/adjust state code
|
||||||
nextStageCode = normalize_stage_code(state_code)
|
normalized_stage_code = normalize_stage_code(state_code)
|
||||||
nextStage = STATUS_CODE_MAP.get(nextStageCode, '')
|
nextStageCode = external_stage_code(
|
||||||
|
client_mode,
|
||||||
|
normalized_stage_code,
|
||||||
|
)
|
||||||
|
nextStage = STATUS_CODE_MAP.get(normalized_stage_code, '')
|
||||||
logger.info(
|
logger.info(
|
||||||
"FastGPT stream stage_code parsed "
|
"FastGPT stream stage_code parsed "
|
||||||
f"sessionId={json_data['sessionId']} "
|
f"sessionId={json_data['sessionId']} "
|
||||||
|
f"clientMode={client_mode} "
|
||||||
|
f"rawStageCode={state_code} "
|
||||||
f"nextStageCode={nextStageCode} nextStage={nextStage}"
|
f"nextStageCode={nextStageCode} nextStage={nextStage}"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -509,13 +524,15 @@ async def chat(
|
|||||||
logger.error(f"content既不是list也不是str, type: {type(content)}")
|
logger.error(f"content既不是list也不是str, type: {type(content)}")
|
||||||
raise ValueError("大模型回复不是list也不是str")
|
raise ValueError("大模型回复不是list也不是str")
|
||||||
|
|
||||||
nextStageCode = content_stage_code or data['newVariables']['status_code']
|
raw_stage_code = content_stage_code or data['newVariables']['status_code']
|
||||||
nextStageCode = normalize_stage_code(nextStageCode)
|
normalized_stage_code = normalize_stage_code(raw_stage_code)
|
||||||
nextStage = STATUS_CODE_MAP.get(nextStageCode, '')
|
nextStageCode = external_stage_code(client_mode, normalized_stage_code)
|
||||||
|
nextStage = STATUS_CODE_MAP.get(normalized_stage_code, '')
|
||||||
form_update = extract_form_update_from_flow_nodes(data.get("responseData", []))
|
form_update = extract_form_update_from_flow_nodes(data.get("responseData", []))
|
||||||
logger.info(
|
logger.info(
|
||||||
"Chat non-stream completed "
|
"Chat non-stream completed "
|
||||||
f"sessionId={json_data['sessionId']} "
|
f"sessionId={json_data['sessionId']} "
|
||||||
|
f"clientMode={client_mode} rawStageCode={raw_stage_code} "
|
||||||
f"duration_ms={(time.perf_counter() - request_started_at) * 1000:.1f} "
|
f"duration_ms={(time.perf_counter() - request_started_at) * 1000:.1f} "
|
||||||
f"nextStageCode={nextStageCode} nextStage={nextStage} "
|
f"nextStageCode={nextStageCode} nextStage={nextStage} "
|
||||||
f"output_len={len(content)} formUpdate_type={type(form_update).__name__} "
|
f"output_len={len(content)} formUpdate_type={type(form_update).__name__} "
|
||||||
@@ -549,10 +566,11 @@ async def chat(
|
|||||||
@router.post("/set_info", response_model=ProcessResponse_set)
|
@router.post("/set_info", response_model=ProcessResponse_set)
|
||||||
async def set_info(
|
async def set_info(
|
||||||
request: ProcessRequest_set,
|
request: ProcessRequest_set,
|
||||||
client: AsyncChatClient = Depends(get_fastgpt_client)
|
|
||||||
):
|
):
|
||||||
"""Set information in chat state."""
|
"""Set information in chat state."""
|
||||||
json_data = request.model_dump()
|
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_payload = {'key': json_data['key'], 'value': json_data['value']}
|
||||||
set_info_input = format_set_info_input(
|
set_info_input = format_set_info_input(
|
||||||
set_info_payload,
|
set_info_payload,
|
||||||
@@ -585,7 +603,11 @@ async def set_info(
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await delete_last_two_chat_records(client, json_data['sessionId'])
|
await delete_last_two_chat_records(
|
||||||
|
client,
|
||||||
|
profile.app_id,
|
||||||
|
json_data['sessionId'],
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error deleting chat records: {e}")
|
logger.error(f"Error deleting chat records: {e}")
|
||||||
return ProcessResponse_set(
|
return ProcessResponse_set(
|
||||||
@@ -614,7 +636,11 @@ async def set_info(
|
|||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|
||||||
# Delete records again after update
|
# Delete records again after update
|
||||||
await delete_last_two_chat_records(client, json_data['sessionId'])
|
await delete_last_two_chat_records(
|
||||||
|
client,
|
||||||
|
profile.app_id,
|
||||||
|
json_data['sessionId'],
|
||||||
|
)
|
||||||
|
|
||||||
return ProcessResponse_set(
|
return ProcessResponse_set(
|
||||||
sessionId=json_data['sessionId'],
|
sessionId=json_data['sessionId'],
|
||||||
@@ -635,10 +661,11 @@ async def set_info(
|
|||||||
@router.post("/get_info", response_model=ProcessResponse_get)
|
@router.post("/get_info", response_model=ProcessResponse_get)
|
||||||
async def get_info(
|
async def get_info(
|
||||||
request: ProcessRequest_get,
|
request: ProcessRequest_get,
|
||||||
client: AsyncChatClient = Depends(get_fastgpt_client)
|
|
||||||
):
|
):
|
||||||
"""Get information from chat state."""
|
"""Get information from chat state."""
|
||||||
json_data = request.model_dump()
|
json_data = request.model_dump()
|
||||||
|
profile = get_fastgpt_profile(json_data['clientMode'])
|
||||||
|
client = profile.client
|
||||||
get_info_payload = {'key': json_data['key']}
|
get_info_payload = {'key': json_data['key']}
|
||||||
get_info_input = format_set_info_input(
|
get_info_input = format_set_info_input(
|
||||||
get_info_payload,
|
get_info_payload,
|
||||||
@@ -672,7 +699,11 @@ async def get_info(
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await delete_last_two_chat_records(client, json_data['sessionId'])
|
await delete_last_two_chat_records(
|
||||||
|
client,
|
||||||
|
profile.app_id,
|
||||||
|
json_data['sessionId'],
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error deleting records: {e}")
|
logger.error(f"Error deleting records: {e}")
|
||||||
return ProcessResponse_get(
|
return ProcessResponse_get(
|
||||||
@@ -737,10 +768,11 @@ async def get_info(
|
|||||||
@router.delete("/delete_session", response_model=ProcessResponse_delete_session)
|
@router.delete("/delete_session", response_model=ProcessResponse_delete_session)
|
||||||
async def delete_session(
|
async def delete_session(
|
||||||
request: ProcessRequest_delete_session,
|
request: ProcessRequest_delete_session,
|
||||||
client: AsyncChatClient = Depends(get_fastgpt_client)
|
|
||||||
):
|
):
|
||||||
"""Delete a chat session."""
|
"""Delete a chat session."""
|
||||||
json_data = request.model_dump()
|
json_data = request.model_dump()
|
||||||
|
profile = get_fastgpt_profile(json_data['clientMode'])
|
||||||
|
client = profile.client
|
||||||
chat_id = json_data.get('sessionId')
|
chat_id = json_data.get('sessionId')
|
||||||
|
|
||||||
if not chat_id:
|
if not chat_id:
|
||||||
@@ -754,7 +786,7 @@ async def delete_session(
|
|||||||
try:
|
try:
|
||||||
# Use SDK's delete_chat_history
|
# Use SDK's delete_chat_history
|
||||||
response = await client.delete_chat_history(
|
response = await client.delete_chat_history(
|
||||||
appId=Config.FASTGPT_APP_ID,
|
appId=profile.app_id,
|
||||||
chatId=chat_id
|
chatId=chat_id
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|||||||
@@ -10,20 +10,42 @@ class Config:
|
|||||||
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./test.db")
|
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./test.db")
|
||||||
DEBUG = os.getenv("DEBUG", "false").lower() in ("true", "1", "t")
|
DEBUG = os.getenv("DEBUG", "false").lower() in ("true", "1", "t")
|
||||||
|
|
||||||
# FastGPT Configuration
|
# FastGPT Configuration. Direct mode falls back to the legacy environment
|
||||||
FASTGPT_API_KEY = os.getenv("ANALYSIS_AUTH_TOKEN")
|
# variable names so existing deployments keep working unchanged.
|
||||||
FASTGPT_BASE_URL = os.getenv("ANALYSIS_SERVICE_URL")
|
FASTGPT_DIRECT_API_KEY = (
|
||||||
FASTGPT_APP_ID = os.getenv("APP_ID")
|
os.getenv("FASTGPT_DIRECT_API_KEY")
|
||||||
DELETE_SESSION_URL = os.getenv("DELETE_SESSION_URL")
|
or os.getenv("ANALYSIS_AUTH_TOKEN")
|
||||||
DELETE_CHAT_URL = os.getenv("DELETE_CHAT_URL")
|
)
|
||||||
GET_CHAT_RECORDS_URL = os.getenv("GET_CHAT_RECORDS_URL")
|
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
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate(cls):
|
def validate(cls):
|
||||||
"""Validate required configuration."""
|
"""Validate required configuration."""
|
||||||
required = [
|
required = [
|
||||||
("FASTGPT_API_KEY", cls.FASTGPT_API_KEY),
|
("FASTGPT_DIRECT_BASE_URL (or ANALYSIS_SERVICE_URL)", cls.FASTGPT_DIRECT_BASE_URL),
|
||||||
("FASTGPT_APP_ID", cls.FASTGPT_APP_ID),
|
("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),
|
||||||
]
|
]
|
||||||
missing = [name for name, value in required if not value]
|
missing = [name for name, value in required if not value]
|
||||||
if missing:
|
if missing:
|
||||||
|
|||||||
@@ -1,39 +1,76 @@
|
|||||||
"""FastGPT client dependency injection."""
|
"""FastGPT client dependency injection."""
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import AsyncExitStack, asynccontextmanager
|
||||||
|
from dataclasses import dataclass
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastgpt_client import AsyncChatClient
|
from fastgpt_client import AsyncChatClient
|
||||||
from .config import Config
|
from .config import Config
|
||||||
|
|
||||||
# Global client instance
|
|
||||||
_fastgpt_client: AsyncChatClient | None = None
|
@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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def lifespan(app: FastAPI):
|
async def lifespan(app: FastAPI):
|
||||||
"""Manage FastGPT client lifecycle."""
|
"""Manage all mode-specific FastGPT clients."""
|
||||||
global _fastgpt_client
|
|
||||||
Config.validate()
|
Config.validate()
|
||||||
|
|
||||||
# Initialize client
|
async with AsyncExitStack() as stack:
|
||||||
_fastgpt_client = AsyncChatClient(
|
for client_mode, settings in _profile_settings().items():
|
||||||
api_key=Config.FASTGPT_API_KEY,
|
client = AsyncChatClient(
|
||||||
base_url=Config.FASTGPT_BASE_URL,
|
api_key=settings["api_key"],
|
||||||
|
base_url=settings["base_url"],
|
||||||
timeout=60.0,
|
timeout=60.0,
|
||||||
max_retries=3,
|
max_retries=3,
|
||||||
retry_delay=1.0,
|
retry_delay=1.0,
|
||||||
enable_logging=Config.DEBUG,
|
enable_logging=Config.DEBUG,
|
||||||
)
|
)
|
||||||
await _fastgpt_client.__aenter__()
|
await stack.enter_async_context(client)
|
||||||
|
_fastgpt_profiles[client_mode] = FastGPTProfile(
|
||||||
|
client=client,
|
||||||
|
app_id=settings["app_id"],
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
yield
|
yield
|
||||||
|
finally:
|
||||||
# Cleanup
|
_fastgpt_profiles.clear()
|
||||||
if _fastgpt_client:
|
|
||||||
await _fastgpt_client.__aexit__(None, None, None)
|
|
||||||
|
|
||||||
|
|
||||||
def get_fastgpt_client() -> AsyncChatClient:
|
def get_fastgpt_profile(client_mode: str) -> FastGPTProfile:
|
||||||
"""Get the FastGPT client instance."""
|
"""Get the initialized FastGPT profile for a client mode."""
|
||||||
if _fastgpt_client is None:
|
try:
|
||||||
raise RuntimeError("FastGPT client not initialized")
|
return _fastgpt_profiles[client_mode]
|
||||||
return _fastgpt_client
|
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
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
from typing import Any, Optional
|
from typing import Any, Literal, Optional
|
||||||
|
|
||||||
|
ClientMode = Literal["direct", "browser_addon"]
|
||||||
|
|
||||||
class ProcessRequest_chat(BaseModel):
|
class ProcessRequest_chat(BaseModel):
|
||||||
sessionId: str = Field(..., max_length=64)
|
sessionId: str = Field(..., max_length=64)
|
||||||
timeStamp: str = Field(..., max_length=32)
|
timeStamp: str = Field(..., max_length=32)
|
||||||
text: str = Field(...)
|
text: str = Field(...)
|
||||||
|
clientMode: ClientMode = "direct"
|
||||||
needFormUpdate: bool = False
|
needFormUpdate: bool = False
|
||||||
useTextChunk: bool = False
|
useTextChunk: bool = False
|
||||||
|
|
||||||
@@ -14,7 +17,7 @@ class ProcessResponse_chat(BaseModel):
|
|||||||
outputText: str = Field(...)
|
outputText: str = Field(...)
|
||||||
formUpdate: Any = Field(default_factory=dict)
|
formUpdate: Any = Field(default_factory=dict)
|
||||||
nextStage: str = Field(..., max_length=32)
|
nextStage: str = Field(..., max_length=32)
|
||||||
nextStageCode: str = Field(..., max_length=4)
|
nextStageCode: str = Field(..., max_length=32)
|
||||||
code: str = Field(..., max_length=4)
|
code: str = Field(..., max_length=4)
|
||||||
msg: Optional[str] = None
|
msg: Optional[str] = None
|
||||||
|
|
||||||
@@ -22,6 +25,7 @@ class ProcessRequest_get(BaseModel):
|
|||||||
sessionId: str = Field(..., max_length=64)
|
sessionId: str = Field(..., max_length=64)
|
||||||
timeStamp: str = Field(..., max_length=32)
|
timeStamp: str = Field(..., max_length=32)
|
||||||
key: str = Field(...)
|
key: str = Field(...)
|
||||||
|
clientMode: ClientMode = "direct"
|
||||||
includeInputInfo: bool = False
|
includeInputInfo: bool = False
|
||||||
|
|
||||||
class ProcessResponse_get(BaseModel):
|
class ProcessResponse_get(BaseModel):
|
||||||
@@ -36,6 +40,7 @@ class ProcessRequest_set(BaseModel):
|
|||||||
timeStamp: str = Field(..., max_length=32)
|
timeStamp: str = Field(..., max_length=32)
|
||||||
key: str = Field(...)
|
key: str = Field(...)
|
||||||
value: str = Field(...)
|
value: str = Field(...)
|
||||||
|
clientMode: ClientMode = "direct"
|
||||||
includeInputInfo: bool = False
|
includeInputInfo: bool = False
|
||||||
|
|
||||||
class ProcessResponse_set(BaseModel):
|
class ProcessResponse_set(BaseModel):
|
||||||
@@ -47,6 +52,7 @@ class ProcessResponse_set(BaseModel):
|
|||||||
class ProcessRequest_delete_session(BaseModel):
|
class ProcessRequest_delete_session(BaseModel):
|
||||||
sessionId: str = Field(..., max_length=64)
|
sessionId: str = Field(..., max_length=64)
|
||||||
timeStamp: str = Field(..., max_length=32)
|
timeStamp: str = Field(..., max_length=32)
|
||||||
|
clientMode: ClientMode = "direct"
|
||||||
|
|
||||||
class ProcessResponse_delete_session(BaseModel):
|
class ProcessResponse_delete_session(BaseModel):
|
||||||
sessionId: str = Field(..., max_length=64)
|
sessionId: str = Field(..., max_length=64)
|
||||||
|
|||||||
85
test/api/fastapi-browser-addon.http
Normal file
85
test/api/fastapi-browser-addon.http
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
@baseUrl = http://127.0.0.1:8000
|
||||||
|
###
|
||||||
|
@sessionId = a1123
|
||||||
|
@timeStamp = 202603310303
|
||||||
|
@clientMode = browser_addon
|
||||||
|
###
|
||||||
|
GET {{baseUrl}}
|
||||||
|
|
||||||
|
HTTP/1.1 200 - OK
|
||||||
|
date: Tue, 28 Jul 2026 00:36:56 GMT
|
||||||
|
server: uvicorn
|
||||||
|
content-length: 32
|
||||||
|
content-type: application/json
|
||||||
|
connection: close
|
||||||
|
###
|
||||||
|
DELETE {{baseUrl}}/delete_session
|
||||||
|
content-type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"sessionId": "{{sessionId}}",
|
||||||
|
"timeStamp": "{{$timestamp}}",
|
||||||
|
"clientMode": "{{clientMode}}"
|
||||||
|
}
|
||||||
|
|
||||||
|
HTTP/1.1 200 - OK
|
||||||
|
date: Tue, 28 Jul 2026 00:36:57 GMT
|
||||||
|
server: uvicorn
|
||||||
|
content-length: 71
|
||||||
|
content-type: application/json
|
||||||
|
connection: close
|
||||||
|
###
|
||||||
|
POST {{baseUrl}}/chat?stream=true
|
||||||
|
content-type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"sessionId": "{{sessionId}}",
|
||||||
|
"timeStamp": "{{timeStamp}}",
|
||||||
|
"text": "hi",
|
||||||
|
"clientMode": "{{clientMode}}",
|
||||||
|
"needFormUpdate": true,
|
||||||
|
"useTextChunk": true
|
||||||
|
}
|
||||||
|
|
||||||
|
HTTP/1.1 200 - OK
|
||||||
|
date: Tue, 28 Jul 2026 00:36:58 GMT
|
||||||
|
server: uvicorn
|
||||||
|
content-type: text/event-stream; charset=utf-8
|
||||||
|
connection: close
|
||||||
|
transfer-encoding: chunked
|
||||||
|
###
|
||||||
|
POST {{baseUrl}}/get_info
|
||||||
|
content-type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"sessionId": "{{sessionId}}",
|
||||||
|
"timeStamp": "{{timeStamp}}",
|
||||||
|
"key": "hphm1",
|
||||||
|
"clientMode": "{{clientMode}}"
|
||||||
|
}
|
||||||
|
|
||||||
|
HTTP/1.1 200 - OK
|
||||||
|
date: Tue, 28 Jul 2026 00:37:07 GMT
|
||||||
|
server: uvicorn
|
||||||
|
content-length: 97
|
||||||
|
content-type: application/json
|
||||||
|
connection: close
|
||||||
|
###
|
||||||
|
POST {{baseUrl}}/set_info
|
||||||
|
content-type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"sessionId": "{{sessionId}}",
|
||||||
|
"timeStamp": "{{timeStamp}}",
|
||||||
|
"key": "hphm1",
|
||||||
|
"value": "沪A8939",
|
||||||
|
"clientMode": "{{clientMode}}"
|
||||||
|
}
|
||||||
|
|
||||||
|
HTTP/1.1 200 - OK
|
||||||
|
date: Tue, 28 Jul 2026 00:37:00 GMT
|
||||||
|
server: uvicorn
|
||||||
|
content-length: 70
|
||||||
|
content-type: application/json
|
||||||
|
connection: close
|
||||||
|
###
|
||||||
76
test/test_client_mode.py
Normal file
76
test/test_client_mode.py
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@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)
|
||||||
|
|
||||||
|
assert request.clientMode == "direct"
|
||||||
|
|
||||||
|
|
||||||
|
def test_browser_addon_client_mode_is_accepted():
|
||||||
|
request = ProcessRequest_chat(
|
||||||
|
sessionId="session-1",
|
||||||
|
timeStamp="1",
|
||||||
|
text="你好",
|
||||||
|
clientMode="browser_addon",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert request.clientMode == "browser_addon"
|
||||||
|
|
||||||
|
|
||||||
|
def test_unknown_client_mode_is_rejected():
|
||||||
|
with pytest.raises(ValidationError):
|
||||||
|
ProcessRequest_chat(
|
||||||
|
sessionId="session-1",
|
||||||
|
timeStamp="1",
|
||||||
|
text="你好",
|
||||||
|
clientMode="unknown",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("client_mode", "raw_code", "expected"),
|
||||||
|
[
|
||||||
|
("direct", "0001", "0001"),
|
||||||
|
("direct", "1002", "1002"),
|
||||||
|
("browser_addon", "0001", "browser_addon.0001"),
|
||||||
|
("browser_addon", "1002", "browser_addon.1002"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_external_stage_code(client_mode, raw_code, expected):
|
||||||
|
assert external_stage_code(client_mode, raw_code) == expected
|
||||||
Reference in New Issue
Block a user