Files
fastgpt-python-sdk/fastgpt_client/base_client.py
Xin Wang ef2614a70a feat: add chat_tui example for FastGPT with Textual interface
- Introduced a new example script `chat_tui.py` that provides a full-screen Textual interface for interacting with FastGPT.
- Implemented streaming chat updates, workflow logging, and modal handling for interactive nodes.
- Enhanced FastGPT client with new streaming capabilities and structured event types for better interaction handling.
- Normalized base URL handling in the client to prevent duplicate `/api` paths.
- Added tests for streaming event parsing and interaction handling.
2026-03-10 15:34:27 +08:00

124 lines
4.3 KiB
Python

"""Base client mixin with retry logic and validation."""
import logging
import time
from typing import Any
class BaseClientMixin:
"""Mixin class providing retry logic, validation, and logging for FastGPT clients."""
@staticmethod
def _normalize_base_url(base_url: str) -> str:
"""Normalize FastGPT base URLs so '/api' is not duplicated by endpoint paths."""
normalized = str(base_url or "").strip().rstrip("/")
if normalized.endswith("/api"):
normalized = normalized[:-4]
return normalized
def __init__(
self,
api_key: str,
base_url: str,
timeout: float = 60.0,
max_retries: int = 3,
retry_delay: float = 1.0,
enable_logging: bool = False,
):
"""Initialize base client.
Args:
api_key: FastGPT API key
base_url: Base URL for FastGPT API
timeout: Request timeout in seconds
max_retries: Maximum number of retry attempts
retry_delay: Delay between retries in seconds
enable_logging: Whether to enable request logging
"""
self.api_key = api_key
self.base_url = self._normalize_base_url(base_url)
self.timeout = timeout
self.max_retries = max_retries
self.retry_delay = retry_delay
self.enable_logging = enable_logging
self.logger = logging.getLogger(__name__)
def _validate_params(self, **params) -> None:
"""Validate request parameters.
Args:
**params: Parameters to validate
Raises:
ValidationError: If any parameter is invalid
"""
for key, value in params.items():
if value is None:
continue
# Check for empty strings that should be non-empty
if isinstance(value, str) and key in ("query", "chatId", "appId", "dataId", "content"):
if not value.strip():
from .exceptions import ValidationError
raise ValidationError(f"{key} must be a non-empty string")
# Check for valid lists/dicts
elif isinstance(value, (list, dict)) and not value:
# Empty lists/dicts are usually valid, but log for debugging
if self.enable_logging and self.logger.isEnabledFor(logging.DEBUG):
self.logger.debug(f"Parameter {key} is empty")
def _retry_request(self, request_func, request_context: str):
"""Execute a request with retry logic.
Args:
request_func: Function that executes the HTTP request
request_context: Description of the request for logging
Returns:
Response from the request
Raises:
APIError: If all retries are exhausted
"""
last_exception = None
for attempt in range(self.max_retries):
try:
response = request_func()
# Success on non-5xx responses
if response.status_code < 500:
return response
# Server error - will retry
if self.enable_logging:
self.logger.warning(
f"{request_context} failed with status {response.status_code} "
f"(attempt {attempt + 1}/{self.max_retries})"
)
if attempt < self.max_retries - 1:
# Exponential backoff
sleep_time = self.retry_delay * (2 ** attempt)
time.sleep(sleep_time)
except Exception as e:
last_exception = e
if self.enable_logging:
self.logger.warning(
f"{request_context} raised exception: {e} "
f"(attempt {attempt + 1}/{self.max_retries})"
)
if attempt < self.max_retries - 1:
sleep_time = self.retry_delay * (2 ** attempt)
time.sleep(sleep_time)
# All retries exhausted
if last_exception:
from .exceptions import APIError
raise APIError(f"Request failed after {self.max_retries} attempts: {last_exception}")
from .exceptions import APIError
raise APIError(f"Request failed after {self.max_retries} attempts")