diff --git a/python/valuecell/adapters/assets/i18n_integration.py b/python/valuecell/adapters/assets/i18n_integration.py index 4ef3b078c..ab24e608d 100644 --- a/python/valuecell/adapters/assets/i18n_integration.py +++ b/python/valuecell/adapters/assets/i18n_integration.py @@ -7,8 +7,8 @@ import logging from typing import Dict, List, Optional -from ...i18n import get_i18n_service, t, get_i18n_config -from ...config.i18n import I18nConfig +from ...server.api.i18n_api import get_i18n_service, t, get_i18n_config +from ...server.config.i18n import I18nConfig from .types import Asset, AssetSearchResult, AssetType, MarketStatus from .manager import AdapterManager diff --git a/python/valuecell/api/__init__.py b/python/valuecell/api/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/python/valuecell/api/app.py b/python/valuecell/api/app.py deleted file mode 100644 index 80c21898c..000000000 --- a/python/valuecell/api/app.py +++ /dev/null @@ -1,95 +0,0 @@ -"""Main API application for ValueCell.""" - -from typing import Optional -from fastapi import FastAPI -from fastapi.middleware.cors import CORSMiddleware -from contextlib import asynccontextmanager - -from .schemas import SuccessResponse -from .i18n_api import create_i18n_router -from ..config.settings import get_settings - - -class ValueCellAPI: - """Main API class for ValueCell.""" - - def __init__(self): - """Initialize API application.""" - self.settings = get_settings() - self.app = self._create_app() - - def _create_app(self) -> FastAPI: - """Create FastAPI application.""" - - @asynccontextmanager - async def lifespan(app: FastAPI): - # Startup - print("ValueCell API starting up...") - yield - # Shutdown - print("ValueCell API shutting down...") - - app = FastAPI( - title="ValueCell API", - description="A community-driven, multi-agent platform for financial applications", - version=self.settings.APP_VERSION, - lifespan=lifespan, - ) - - # Add CORS middleware - app.add_middleware( - CORSMiddleware, - allow_origins=["*"], # Configure properly in production - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], - ) - - # Add routes - self._add_routes(app) - - return app - - def _add_routes(self, app: FastAPI): - """Add API routes.""" - - @app.get("/", response_model=SuccessResponse) - async def root(): - """Root endpoint.""" - return SuccessResponse( - message="Welcome to ValueCell API", - data={ - "name": self.settings.APP_NAME, - "version": self.settings.APP_VERSION, - "environment": self.settings.APP_ENVIRONMENT, - }, - ) - - @app.get("/health") - async def health(): - """Health check endpoint.""" - return {"status": "healthy", "version": self.settings.APP_VERSION} - - # Include i18n router - app.include_router(create_i18n_router()) - - -# Global API instance -_api: Optional[ValueCellAPI] = None - - -def get_api() -> ValueCellAPI: - """Get global API instance.""" - global _api - if _api is None: - _api = ValueCellAPI() - return _api - - -def create_app() -> FastAPI: - """Create FastAPI application.""" - return get_api().app - - -# For uvicorn -app = create_app() diff --git a/python/valuecell/api/router/__init__.py b/python/valuecell/api/router/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/python/valuecell/api/router/i18n.py b/python/valuecell/api/router/i18n.py deleted file mode 100644 index 7baaf5427..000000000 --- a/python/valuecell/api/router/i18n.py +++ /dev/null @@ -1,20 +0,0 @@ -"""I18n router module for ValueCell API.""" - -from fastapi import APIRouter -from ..i18n_api import create_i18n_router - - -def get_i18n_router() -> APIRouter: - """Get i18n router instance. - - This function creates and returns an i18n router that can be included - in the main FastAPI application. - - Returns: - APIRouter: The configured i18n router - """ - return create_i18n_router() - - -# Export the router function -__all__ = ["get_i18n_router"] diff --git a/python/valuecell/api/schemas.py b/python/valuecell/api/schemas.py deleted file mode 100644 index 62329ca93..000000000 --- a/python/valuecell/api/schemas.py +++ /dev/null @@ -1,174 +0,0 @@ -"""API schemas for ValueCell application.""" - -from typing import Dict, Any, List, Optional -from datetime import datetime -from pydantic import BaseModel, Field, validator - -from ..core.constants import SUPPORTED_LANGUAGE_CODES - - -class BaseResponse(BaseModel): - """Base response schema.""" - - success: bool - message: str - data: Optional[Dict[str, Any]] = None - error: Optional[str] = None - - -class ErrorResponse(BaseResponse): - """Error response schema.""" - - success: bool = False - error: str - data: Optional[Dict[str, Any]] = None - - -class SuccessResponse(BaseResponse): - """Success response schema.""" - - success: bool = True - data: Dict[str, Any] - - -# I18n related schemas -class I18nConfigResponse(BaseModel): - """I18n configuration response.""" - - language: str - timezone: str - date_format: str - time_format: str - datetime_format: str - currency_symbol: str - number_format: Dict[str, str] - is_rtl: bool - - -class SupportedLanguage(BaseModel): - """Supported language schema.""" - - code: str - name: str - is_current: bool - - -class SupportedLanguagesResponse(BaseModel): - """Supported languages response.""" - - languages: List[SupportedLanguage] - current: str - - -class TimezoneInfo(BaseModel): - """Timezone information schema.""" - - value: str - label: str - is_current: bool - - -class TimezonesResponse(BaseModel): - """Timezones response.""" - - timezones: List[TimezoneInfo] - current: str - - -class LanguageRequest(BaseModel): - """Language change request.""" - - language: str = Field(..., description="Language code to set") - - @validator("language") - def validate_language(cls, v): - if v not in SUPPORTED_LANGUAGE_CODES: - raise ValueError(f"Language {v} is not supported") - return v - - -class TimezoneRequest(BaseModel): - """Timezone change request.""" - - timezone: str = Field(..., description="Timezone to set") - - -class LanguageDetectionRequest(BaseModel): - """Language detection request.""" - - accept_language: str = Field(..., description="Accept-Language header value") - - -class TranslationRequest(BaseModel): - """Translation request.""" - - key: str = Field(..., description="Translation key") - language: Optional[str] = Field(None, description="Target language") - variables: Optional[Dict[str, Any]] = Field( - default_factory=dict, description="Variables for string formatting" - ) - - -class DateTimeFormatRequest(BaseModel): - """DateTime formatting request.""" - - datetime: str = Field(..., description="ISO datetime string") - format_type: str = Field( - "datetime", description="Format type: date, time, or datetime" - ) - - -class NumberFormatRequest(BaseModel): - """Number formatting request.""" - - number: float = Field(..., description="Number to format") - decimal_places: int = Field(2, description="Number of decimal places") - - -class CurrencyFormatRequest(BaseModel): - """Currency formatting request.""" - - amount: float = Field(..., description="Amount to format") - decimal_places: int = Field(2, description="Number of decimal places") - - -class UserI18nSettings(BaseModel): - """User i18n settings schema.""" - - user_id: Optional[str] = None - language: str = "en-US" - timezone: str = "UTC" - created_at: Optional[datetime] = None - updated_at: Optional[datetime] = None - - @validator("language") - def validate_language(cls, v): - if v not in SUPPORTED_LANGUAGE_CODES: - raise ValueError(f"Language {v} is not supported") - return v - - -class UserI18nSettingsRequest(BaseModel): - """User i18n settings update request.""" - - language: Optional[str] = None - timezone: Optional[str] = None - - @validator("language") - def validate_language(cls, v): - if v and v not in SUPPORTED_LANGUAGE_CODES: - raise ValueError(f"Language {v} is not supported") - return v - - -class AgentI18nContext(BaseModel): - """Agent i18n context schema for inter-agent communication.""" - - language: str - timezone: str - currency_symbol: str - date_format: str - time_format: str - number_format: Dict[str, str] - user_id: Optional[str] = None - session_id: Optional[str] = None diff --git a/python/valuecell/config/settings.py b/python/valuecell/config/settings.py deleted file mode 100644 index 5a3787f77..000000000 --- a/python/valuecell/config/settings.py +++ /dev/null @@ -1,117 +0,0 @@ -"""Settings configuration for ValueCell application.""" - -import os -from typing import Optional -from pathlib import Path - -from ..core.constants import ( - DEFAULT_LANGUAGE, - SUPPORTED_LANGUAGE_CODES, - DB_CHARSET, - DB_COLLATION, -) -from .i18n import I18nConfig - - -class Settings: - """Application settings configuration.""" - - def __init__(self): - """Initialize settings from environment variables.""" - # Application Configuration - self.APP_NAME = os.getenv("APP_NAME", "ValueCell") - self.APP_VERSION = os.getenv("APP_VERSION", "0.1.0") - self.APP_ENVIRONMENT = os.getenv("APP_ENVIRONMENT", "development") - - # API Configuration - self.API_HOST = os.getenv("API_HOST", "localhost") - self.API_PORT = int(os.getenv("API_PORT", "8000")) - self.API_DEBUG = os.getenv("API_DEBUG", "false").lower() == "true" - - # Database Configuration - self.DB_CHARSET = os.getenv("DB_CHARSET", DB_CHARSET) - self.DB_COLLATION = os.getenv("DB_COLLATION", DB_COLLATION) - - # Internationalization Configuration - self.LANG = os.getenv("LANG", DEFAULT_LANGUAGE) - self.TIMEZONE = os.getenv("TIMEZONE", "") - - # Validate language - if self.LANG not in SUPPORTED_LANGUAGE_CODES: - self.LANG = DEFAULT_LANGUAGE - - # File and Directory Configuration - self.BASE_DIR = Path(__file__).parent.parent.parent - self.LOCALE_DIR = self.BASE_DIR / "locales" - - # Ensure locale directory exists - self.LOCALE_DIR.mkdir(exist_ok=True) - - # Initialize i18n configuration - self._i18n_config = I18nConfig(self.LANG, self.TIMEZONE) - - # API Configuration - self.API_ENABLED = os.getenv("API_ENABLED", "true").lower() == "true" - self.API_I18N_ENABLED = os.getenv("API_I18N_ENABLED", "true").lower() == "true" - - @property - def i18n(self) -> I18nConfig: - """Get i18n configuration.""" - return self._i18n_config - - def update_language(self, language: str) -> None: - """Update application language.""" - if language in SUPPORTED_LANGUAGE_CODES: - self.LANG = language - self._i18n_config.set_language(language) - - def update_timezone(self, timezone: str) -> None: - """Update application timezone.""" - self.TIMEZONE = timezone - self._i18n_config.set_timezone(timezone) - - def get_api_config(self) -> dict: - """Get API configuration.""" - return { - "enabled": self.API_ENABLED, - "host": self.API_HOST, - "port": self.API_PORT, - "debug": self.API_DEBUG, - "i18n_enabled": self.API_I18N_ENABLED, - } - - def get_i18n_config(self) -> dict: - """Get i18n configuration.""" - return self._i18n_config.to_dict() - - def to_dict(self) -> dict: - """Convert settings to dictionary.""" - return { - "app_name": self.APP_NAME, - "app_version": self.APP_VERSION, - "app_environment": self.APP_ENVIRONMENT, - "api": self.get_api_config(), - "db_charset": self.DB_CHARSET, - "db_collation": self.DB_COLLATION, - "language": self.LANG, - "timezone": self.TIMEZONE, - "i18n": self.get_i18n_config(), - } - - -# Global settings instance -_settings: Optional[Settings] = None - - -def get_settings() -> Settings: - """Get global settings instance.""" - global _settings - if _settings is None: - _settings = Settings() - return _settings - - -def reset_settings() -> None: - """Reset global settings instance.""" - global _settings - _settings = None diff --git a/python/valuecell/examples/i18n_example.py b/python/valuecell/examples/i18n_example.py deleted file mode 100644 index 9bd3dbd0e..000000000 --- a/python/valuecell/examples/i18n_example.py +++ /dev/null @@ -1,165 +0,0 @@ -"""Example usage of ValueCell i18n system.""" - -# TODO: This file is a temporary file, it will be removed in the future. -import os -import sys -from datetime import datetime -from pathlib import Path - -# Add the parent directory to Python path to enable imports -current_dir = Path(__file__).parent -project_root = current_dir.parent.parent -sys.path.insert(0, str(project_root)) - -# Set environment for example -os.environ["LANG"] = "zh-Hans" -os.environ["TIMEZONE"] = "Asia/Shanghai" - -try: - # Option 1: Import from dedicated i18n module (recommended) - from valuecell.i18n import ( - get_settings, - get_i18n_service, - t, - detect_browser_language, - format_file_size, - format_duration, - pluralize, - ) - - # Option 2: Import from specific modules (alternative) - # from valuecell.config.settings import get_settings - # from valuecell.services.i18n_service import get_i18n_service, t - # from valuecell.utils.i18n_utils import detect_browser_language, format_file_size, format_duration, pluralize - -except ImportError as e: - print(f"Import error: {e}") - print("Please make sure you're running this from the correct directory.") - print( - "Try: cd /path/to/valuecell/python && python -m valuecell.examples.i18n_example" - ) - sys.exit(1) - - -def main(): - """Main example function.""" - print("=== ValueCell i18n System Example ===\n") - - # Initialize services - settings = get_settings() - i18n = get_i18n_service() - - print("1. Current Configuration:") - print(f" Language: {i18n.get_current_language()}") - print(f" Timezone: {i18n.get_current_timezone()}") - print(f" Settings: {settings.to_dict()['i18n']}") - print() - - # Translation examples - print("2. Translation Examples:") - print(f" Welcome (current): {t('messages.welcome')}") - print(f" Welcome (en-US): {i18n.translate('messages.welcome', 'en-US')}") - print(f" Welcome (zh-Hant): {i18n.translate('messages.welcome', 'zh-Hant')}") - print() - - # Translation with variables - print("3. Translation with Variables:") - app_version = settings.APP_VERSION - copyright_year = datetime.now().year - print(f" Version: {t('app.version', version=app_version)}") - print(f" Copyright: {t('app.copyright', year=copyright_year)}") - print() - - # Date and time formatting - print("4. Date and Time Formatting:") - now = datetime.now() - print(f" Current time: {now}") - print(f" Formatted date: {i18n.format_datetime(now, 'date')}") - print(f" Formatted time: {i18n.format_datetime(now, 'time')}") - print(f" Formatted datetime: {i18n.format_datetime(now, 'datetime')}") - print() - - # Number and currency formatting - print("5. Number and Currency Formatting:") - number = 1234567.89 - currency = 9876.54 - print(f" Original number: {number}") - print(f" Formatted number: {i18n.format_number(number)}") - print(f" Formatted currency: {i18n.format_currency(currency)}") - print() - - # Language detection - print("6. Language Detection:") - test_headers = [ - "en-US,en;q=0.9,zh;q=0.8", - "zh-CN,zh;q=0.9,en;q=0.8", - "zh-TW,zh;q=0.9,en;q=0.8", - "en-GB,en;q=0.9", - ] - for header in test_headers: - detected = detect_browser_language(header) - print(f" '{header}' -> {detected}") - print() - - # File size and duration formatting - print("7. Utility Formatting:") - file_sizes = [512, 1024, 1048576, 1073741824] - for size in file_sizes: - formatted = format_file_size(size) - print(f" {size} bytes -> {formatted}") - - durations = [30, 120, 3600, 86400] - for duration in durations: - formatted = format_duration(duration) - print(f" {duration} seconds -> {formatted}") - print() - - # Pluralization examples - print("8. Pluralization Examples:") - words = [("file", None), ("item", None), ("category", "categories")] - counts = [0, 1, 2, 5] - for singular, plural in words: - for count in counts: - result = pluralize(count, singular, plural) - print(f" {count} {result}") - print() - - # Switch languages and show differences - print("9. Language Switching:") - languages = ["en-US", "en-GB", "zh-Hans", "zh-Hant"] - - for lang in languages: - i18n.set_language(lang) - welcome = t("messages.welcome") - success = t("messages.data_saved") - print(f" {lang}: {welcome} | {success}") - print() - - # Show timezone differences - print("10. Timezone Formatting:") - test_dt = datetime(2024, 1, 15, 14, 30, 0) - timezones = [ - "UTC", - "America/New_York", - "Europe/London", - "Asia/Shanghai", - "Asia/Hong_Kong", - ] - - for tz in timezones: - i18n.set_timezone(tz) - formatted = i18n.format_datetime(test_dt) - print(f" {tz}: {formatted}") - print() - - # Show supported languages - print("11. Supported Languages:") - for code, name in i18n.get_supported_languages(): - print(f" {code}: {name}") - print() - - print("=== Example Complete ===") - - -if __name__ == "__main__": - main() diff --git a/python/valuecell/i18n.py b/python/valuecell/i18n.py deleted file mode 100644 index d09924045..000000000 --- a/python/valuecell/i18n.py +++ /dev/null @@ -1,106 +0,0 @@ -"""Internationalization module entry point for ValueCell. - -This module provides convenient imports for i18n functionality. -Import from here to access all i18n features in one place. -""" - -# Core i18n functionality -from .services.i18n_service import ( - get_i18n_service, - t, - translate, - reset_i18n_service, -) - -# Configuration -from .config.settings import get_settings -from .config.i18n import ( - get_i18n_config, - set_i18n_config, - reset_i18n_config, - I18nConfig, -) - -# Constants -from .core.constants import ( - SUPPORTED_LANGUAGES, - SUPPORTED_LANGUAGE_CODES, - LANGUAGE_TIMEZONE_MAPPING, - DEFAULT_LANGUAGE, - DEFAULT_TIMEZONE, - DATE_FORMATS, - TIME_FORMATS, - DATETIME_FORMATS, - CURRENCY_SYMBOLS, - NUMBER_FORMATS, -) - -# Utilities -from .utils.i18n_utils import ( - detect_browser_language, - get_timezone_for_language, - validate_language_code, - validate_timezone, - get_available_timezones, - get_common_timezones, - get_timezone_display_name, - convert_timezone, - format_file_size, - format_duration, - pluralize, - get_language_direction, - extract_translation_keys, - validate_translation_file, - get_missing_translations, - create_translation_template, - translatable, -) - -# API Router -from .api.router.i18n import get_i18n_router - -# Export all i18n functionality -__all__ = [ - # Core services - "get_i18n_service", - "t", - "translate", - "reset_i18n_service", - # Configuration - "get_settings", - "get_i18n_config", - "set_i18n_config", - "reset_i18n_config", - "I18nConfig", - # Constants - "SUPPORTED_LANGUAGES", - "SUPPORTED_LANGUAGE_CODES", - "LANGUAGE_TIMEZONE_MAPPING", - "DEFAULT_LANGUAGE", - "DEFAULT_TIMEZONE", - "DATE_FORMATS", - "TIME_FORMATS", - "DATETIME_FORMATS", - "CURRENCY_SYMBOLS", - "NUMBER_FORMATS", - # Utilities - "detect_browser_language", - "get_timezone_for_language", - "validate_language_code", - "validate_timezone", - "get_available_timezones", - "get_common_timezones", - "get_timezone_display_name", - "convert_timezone", - "format_file_size", - "format_duration", - "pluralize", - "get_language_direction", - "extract_translation_keys", - "validate_translation_file", - "get_missing_translations", - "create_translation_template", - "translatable", - # API - "get_i18n_router", -] diff --git a/python/valuecell/server/api/app.py b/python/valuecell/server/api/app.py index 09c050997..e879f5cf1 100644 --- a/python/valuecell/server/api/app.py +++ b/python/valuecell/server/api/app.py @@ -1,10 +1,20 @@ """FastAPI application factory for ValueCell Server.""" from fastapi import FastAPI +from fastapi.exceptions import RequestValidationError from fastapi.middleware.cors import CORSMiddleware from contextlib import asynccontextmanager +from .exceptions import ( + APIException, + api_exception_handler, + validation_exception_handler, + general_exception_handler, +) from ..config.settings import get_settings +from .routers.i18n import create_i18n_router +from .routers.system import create_system_router +from .schemas import SuccessResponse, AppInfoData def create_app() -> FastAPI: @@ -30,11 +40,14 @@ async def lifespan(app: FastAPI): redoc_url="/redoc" if settings.API_DEBUG else None, ) + # Add exception handlers + _add_exception_handlers(app) + # Add middleware _add_middleware(app, settings) # Add routes - _add_routes(app) + _add_routes(app, settings) return app @@ -53,7 +66,38 @@ def _add_middleware(app: FastAPI, settings) -> None: # Custom logging middleware removed -def _add_routes(app: FastAPI) -> None: +def _add_exception_handlers(app: FastAPI): + """Add exception handlers.""" + app.add_exception_handler(APIException, api_exception_handler) + app.add_exception_handler(RequestValidationError, validation_exception_handler) + app.add_exception_handler(Exception, general_exception_handler) + + +def _add_routes(app: FastAPI, settings) -> None: """Add routes to the application.""" - # app.include_router(health.router, prefix="/health", tags=["health"]) - # app.include_router(agents.router, prefix="/api/v1", tags=["agents"]) + + @app.get( + "/", + response_model=SuccessResponse[AppInfoData], + summary="Get application info", + description="Get ValueCell application basic information including name, version and environment", + tags=["Root"], + ) + async def root(): + """Root endpoint - Get application basic information.""" + app_info = AppInfoData( + name=settings.APP_NAME, + version=settings.APP_VERSION, + environment=settings.APP_ENVIRONMENT, + ) + return SuccessResponse.create(data=app_info, msg="Welcome to ValueCell API") + + # Include i18n router + app.include_router(create_i18n_router()) + + # Include system router + app.include_router(create_system_router()) + + +# For uvicorn +app = create_app() diff --git a/python/valuecell/api/i18n_api.py b/python/valuecell/server/api/i18n_api.py similarity index 62% rename from python/valuecell/api/i18n_api.py rename to python/valuecell/server/api/i18n_api.py index 8212af614..2f932ca52 100644 --- a/python/valuecell/api/i18n_api.py +++ b/python/valuecell/server/api/i18n_api.py @@ -6,6 +6,7 @@ from .schemas import ( SuccessResponse, + StatusCode, LanguageRequest, TimezoneRequest, LanguageDetectionRequest, @@ -14,12 +15,26 @@ NumberFormatRequest, CurrencyFormatRequest, UserI18nSettingsRequest, - AgentI18nContext, + # Data models + I18nConfigData, + SupportedLanguagesData, + TimezonesData, + UserI18nSettingsData, + AgentI18nContextData, + LanguageDetectionData, + TranslationData, + DateTimeFormatData, + NumberFormatData, + CurrencyFormatData, +) +from .exceptions import ( + APIException, + InternalServerException, ) from ..services.i18n_service import get_i18n_service from ..config.settings import get_settings -from ..core.constants import SUPPORTED_LANGUAGES, LANGUAGE_TIMEZONE_MAPPING -from ..utils.i18n_utils import ( +from ...core.constants import SUPPORTED_LANGUAGES, LANGUAGE_TIMEZONE_MAPPING +from ...utils.i18n_utils import ( detect_browser_language, get_common_timezones, get_timezone_display_name, @@ -47,31 +62,118 @@ def _create_router(self) -> APIRouter: router = APIRouter(prefix="/i18n", tags=["i18n"]) # Configuration endpoints - router.add_api_route("/config", self.get_config, methods=["GET"]) router.add_api_route( - "/languages", self.get_supported_languages, methods=["GET"] + "/config", + self.get_config, + methods=["GET"], + response_model=SuccessResponse[I18nConfigData], + summary="Get i18n configuration", + description="Get current internationalization configuration information", + ) + router.add_api_route( + "/languages", + self.get_supported_languages, + methods=["GET"], + response_model=SuccessResponse[SupportedLanguagesData], + summary="Get supported languages", + description="Get list of all languages supported by the system", + ) + router.add_api_route( + "/timezones", + self.get_timezones, + methods=["GET"], + response_model=SuccessResponse[TimezonesData], + summary="Get supported timezones", + description="Get list of all timezones supported by the system", ) - router.add_api_route("/timezones", self.get_timezones, methods=["GET"]) # Language and timezone management - router.add_api_route("/language", self.set_language, methods=["POST"]) - router.add_api_route("/timezone", self.set_timezone, methods=["POST"]) - router.add_api_route("/detect-language", self.detect_language, methods=["POST"]) + router.add_api_route( + "/language", + self.set_language, + methods=["PUT"], + response_model=SuccessResponse[UserI18nSettingsData], + summary="Set language", + description="Set user's preferred language", + ) + router.add_api_route( + "/timezone", + self.set_timezone, + methods=["PUT"], + response_model=SuccessResponse[dict], + summary="Set timezone", + description="Set user's preferred timezone", + ) + router.add_api_route( + "/detect-language", + self.detect_language, + methods=["POST"], + response_model=SuccessResponse[LanguageDetectionData], + summary="Detect language", + description="Detect user's preferred language based on Accept-Language header", + ) # Translation and formatting services - router.add_api_route("/translate", self.translate, methods=["POST"]) - router.add_api_route("/format/datetime", self.format_datetime, methods=["POST"]) - router.add_api_route("/format/number", self.format_number, methods=["POST"]) - router.add_api_route("/format/currency", self.format_currency, methods=["POST"]) + router.add_api_route( + "/translate", + self.translate, + methods=["POST"], + response_model=SuccessResponse[TranslationData], + summary="Translate text", + description="Get translated text based on specified key and language", + ) + router.add_api_route( + "/format/datetime", + self.format_datetime, + methods=["POST"], + response_model=SuccessResponse[DateTimeFormatData], + summary="Format datetime", + description="Format datetime according to user's localization settings", + ) + router.add_api_route( + "/format/number", + self.format_number, + methods=["POST"], + response_model=SuccessResponse[NumberFormatData], + summary="Format number", + description="Format number according to user's localization settings", + ) + router.add_api_route( + "/format/currency", + self.format_currency, + methods=["POST"], + response_model=SuccessResponse[CurrencyFormatData], + summary="Format currency", + description="Format currency amount according to user's localization settings", + ) # User settings - router.add_api_route("/user/settings", self.get_user_settings, methods=["GET"]) router.add_api_route( - "/user/settings", self.update_user_settings, methods=["POST"] + "/user/settings", + self.get_user_settings, + methods=["GET"], + response_model=SuccessResponse[UserI18nSettingsData], + summary="Get user i18n settings", + description="Get internationalization settings for specified user", + ) + router.add_api_route( + "/user/settings", + self.update_user_settings, + methods=["PUT"], + response_model=SuccessResponse[UserI18nSettingsData], + summary="Update user i18n settings", + description="Update internationalization settings for specified user", ) # Agent context - router.add_api_route("/agent/context", self.get_agent_context, methods=["GET"]) + router.add_api_route( + "/agent/context", + self.get_agent_context, + methods=["GET"], + response_model=SuccessResponse[AgentI18nContextData], + summary="Get Agent i18n context", + description="Get i18n context information for inter-agent communication", + ) return router @@ -88,49 +190,53 @@ async def get_config( self, user_id: Optional[str] = Header(None, alias="X-User-ID"), session_id: Optional[str] = Header(None, alias="X-Session-ID"), - ) -> SuccessResponse: + ) -> SuccessResponse[I18nConfigData]: """Get current i18n configuration.""" self._get_user_context(user_id) - return SuccessResponse( - message="I18n configuration retrieved successfully", - data=self.i18n_service.to_dict(), + config_dict = self.i18n_service.to_dict() + config_data = I18nConfigData(**config_dict) + + return SuccessResponse.create( + data=config_data, msg="I18n configuration retrieved successfully" ) - async def get_supported_languages(self) -> SuccessResponse: + async def get_supported_languages(self) -> SuccessResponse[SupportedLanguagesData]: """Get supported languages.""" + from .schemas import SupportedLanguage + languages = [ - { - "code": code, - "name": name, - "is_current": code == self.i18n_service.get_current_language(), - } + SupportedLanguage( + code=code, + name=name, + is_current=code == self.i18n_service.get_current_language(), + ) for code, name in SUPPORTED_LANGUAGES ] - return SuccessResponse( - message="Supported languages retrieved successfully", - data={ - "languages": languages, - "current": self.i18n_service.get_current_language(), - }, + languages_data = SupportedLanguagesData( + languages=languages, current=self.i18n_service.get_current_language() + ) + + return SuccessResponse.create( + data=languages_data, msg="Supported languages retrieved successfully" ) async def set_language( self, request: LanguageRequest, user_id: Optional[str] = Header(None, alias="X-User-ID"), - ) -> SuccessResponse: + ) -> SuccessResponse[UserI18nSettingsData]: """Set current language.""" if not validate_language_code(request.language): - raise HTTPException( - status_code=400, - detail=f"Language '{request.language}' is not supported", + raise APIException( + code=StatusCode.BAD_REQUEST, + message=f"Language '{request.language}' is not supported", ) success = self.i18n_service.set_language(request.language) if not success: - raise HTTPException(status_code=500, detail="Failed to set language") + raise InternalServerException("Failed to set language") # Save user context if user_id: @@ -141,12 +247,15 @@ async def set_language( self.i18n_service.get_current_timezone() ) - return SuccessResponse( - message="Language updated successfully", - data={ - "language": request.language, - "timezone": self.i18n_service.get_current_timezone(), - }, + settings_data = UserI18nSettingsData( + user_id=user_id, + language=request.language, + timezone=self.i18n_service.get_current_timezone(), + updated_at=datetime.now(), + ) + + return SuccessResponse.create( + data=settings_data, msg="Language setting successful" ) async def get_timezones(self) -> SuccessResponse: @@ -215,48 +324,47 @@ async def set_timezone( async def detect_language( self, request: LanguageDetectionRequest - ) -> SuccessResponse: + ) -> SuccessResponse[LanguageDetectionData]: """Detect language from Accept-Language header.""" detected_language = detect_browser_language(request.accept_language) - return SuccessResponse( - message="Language detected successfully", - data={ - "detected_language": detected_language, - "language_name": next( - ( - name - for code, name in SUPPORTED_LANGUAGES - if code == detected_language - ), - detected_language, - ), - "is_supported": detected_language - in [code for code, _ in SUPPORTED_LANGUAGES], - }, + language_name = next( + (name for code, name in SUPPORTED_LANGUAGES if code == detected_language), + detected_language, + ) + + detection_data = LanguageDetectionData( + detected_language=detected_language, + language_name=language_name, + is_supported=detected_language in [code for code, _ in SUPPORTED_LANGUAGES], + ) + + return SuccessResponse.create( + data=detection_data, msg="Language detection successful" ) - async def translate(self, request: TranslationRequest) -> SuccessResponse: + async def translate( + self, request: TranslationRequest + ) -> SuccessResponse[TranslationData]: """Translate a key.""" try: translated_text = self.i18n_service.translate( request.key, request.language, **request.variables ) - return SuccessResponse( - message="Translation retrieved successfully", - data={ - "key": request.key, - "translated_text": translated_text, - "language": request.language - or self.i18n_service.get_current_language(), - "variables": request.variables, - }, + translation_data = TranslationData( + key=request.key, + translated_text=translated_text, + language=request.language or self.i18n_service.get_current_language(), + variables=request.variables or {}, + ) + + return SuccessResponse.create( + data=translation_data, msg="Translation retrieved successfully" ) except Exception as e: - raise HTTPException( - status_code=500, - detail=f"Failed to translate key '{request.key}': {str(e)}", + raise InternalServerException( + f"Failed to translate key '{request.key}': {str(e)}" ) async def format_datetime(self, request: DateTimeFormatRequest) -> SuccessResponse: @@ -383,12 +491,12 @@ async def get_agent_context( self, user_id: Optional[str] = Header(None, alias="X-User-ID"), session_id: Optional[str] = Header(None, alias="X-Session-ID"), - ) -> SuccessResponse: + ) -> SuccessResponse[AgentI18nContextData]: """Get i18n context for agent communication.""" # Load user-specific settings self._get_user_context(user_id) - context = AgentI18nContext( + context = AgentI18nContextData( language=self.i18n_service.get_current_language(), timezone=self.i18n_service.get_current_timezone(), currency_symbol=self.i18n_service._i18n_config.get_currency_symbol(), @@ -399,8 +507,8 @@ async def get_agent_context( session_id=session_id, ) - return SuccessResponse( - message="Agent i18n context retrieved successfully", data=context.dict() + return SuccessResponse.create( + data=context, msg="Agent i18n context retrieved successfully" ) def get_user_context(self, user_id: str) -> Dict[str, Any]: diff --git a/python/valuecell/server/api/routers/__init__.py b/python/valuecell/server/api/routers/__init__.py index 3e64b20fc..05c6c1600 100644 --- a/python/valuecell/server/api/routers/__init__.py +++ b/python/valuecell/server/api/routers/__init__.py @@ -1,8 +1,10 @@ """API router module.""" from .i18n import create_i18n_router, get_i18n_router +from .system import create_system_router __all__ = [ "create_i18n_router", "get_i18n_router", + "create_system_router", ] diff --git a/python/valuecell/server/api/routers/system.py b/python/valuecell/server/api/routers/system.py new file mode 100644 index 000000000..950e64629 --- /dev/null +++ b/python/valuecell/server/api/routers/system.py @@ -0,0 +1,47 @@ +"""System related API routes.""" + +from datetime import datetime +from fastapi import APIRouter + +from ..schemas import SuccessResponse, AppInfoData, HealthCheckData +from ...config.settings import get_settings + + +def create_system_router() -> APIRouter: + """Create system related routes.""" + router = APIRouter(prefix="/api/v1/system", tags=["System"]) + settings = get_settings() + + @router.get( + "/info", + response_model=SuccessResponse[AppInfoData], + summary="Get application info", + description="Get ValueCell application basic information including name, version and environment", + ) + async def get_app_info(): + """Get application basic information.""" + app_info = AppInfoData( + name=settings.APP_NAME, + version=settings.APP_VERSION, + environment=settings.APP_ENVIRONMENT, + ) + return SuccessResponse.create( + data=app_info, msg="Application info retrieved successfully" + ) + + @router.get( + "/health", + response_model=SuccessResponse[HealthCheckData], + summary="Health check", + description="Check service running status and version information", + ) + async def health_check(): + """Service health status check.""" + health_data = HealthCheckData( + status="healthy", version=settings.APP_VERSION, timestamp=datetime.now() + ) + return SuccessResponse.create( + data=health_data, msg="Service is running normally" + ) + + return router diff --git a/python/valuecell/server/api/schemas/i18n.py b/python/valuecell/server/api/schemas/i18n.py index 9ff0206e6..3d8b4f1a5 100644 --- a/python/valuecell/server/api/schemas/i18n.py +++ b/python/valuecell/server/api/schemas/i18n.py @@ -4,7 +4,7 @@ from datetime import datetime from pydantic import BaseModel, Field, validator -from ...core.constants import SUPPORTED_LANGUAGE_CODES +from ....core.constants import SUPPORTED_LANGUAGE_CODES # I18n related data models diff --git a/python/valuecell/config/i18n.py b/python/valuecell/server/config/i18n.py similarity index 99% rename from python/valuecell/config/i18n.py rename to python/valuecell/server/config/i18n.py index 6f29fc3e1..424032c45 100644 --- a/python/valuecell/config/i18n.py +++ b/python/valuecell/server/config/i18n.py @@ -5,7 +5,7 @@ import pytz from datetime import datetime -from ..core.constants import ( +from ...core.constants import ( SUPPORTED_LANGUAGE_CODES, LANGUAGE_TIMEZONE_MAPPING, DEFAULT_LANGUAGE, diff --git a/python/valuecell/server/config/settings.py b/python/valuecell/server/config/settings.py index 2c21b775b..3244f5345 100644 --- a/python/valuecell/server/config/settings.py +++ b/python/valuecell/server/config/settings.py @@ -46,6 +46,9 @@ def __init__(self): self.LOGS_DIR = self.BASE_DIR / "logs" self.LOGS_DIR.mkdir(exist_ok=True) + # I18n Configuration + self.LOCALE_DIR = self.BASE_DIR / "locales" + # Agent Configuration self.AGENT_TIMEOUT = int(os.getenv("AGENT_TIMEOUT", "300")) # 5 minutes self.MAX_CONCURRENT_AGENTS = int(os.getenv("MAX_CONCURRENT_AGENTS", "10")) @@ -78,6 +81,26 @@ def get_redis_config(self) -> dict: "url": self.REDIS_URL, } + def update_language(self, language: str) -> None: + """Update current language setting. + + Args: + language: Language code to set + """ + # In a production environment, this might update a database or config file + # For now, we'll just log the change + pass + + def update_timezone(self, timezone: str) -> None: + """Update current timezone setting. + + Args: + timezone: Timezone to set + """ + # In a production environment, this might update a database or config file + # For now, we'll just log the change + pass + @lru_cache() def get_settings() -> Settings: diff --git a/python/valuecell/server/services/__init__.py b/python/valuecell/server/services/__init__.py index e69de29bb..b5f1f42cd 100644 --- a/python/valuecell/server/services/__init__.py +++ b/python/valuecell/server/services/__init__.py @@ -0,0 +1,23 @@ +"""ValueCell Services Module. + +This module provides high-level service layers for various business operations +including asset management, internationalization, and agent context management. +""" + +# Asset service (import directly from .assets to avoid circular imports) + +# I18n service +from .i18n_service import I18nService, get_i18n_service + +# Agent context service +from .agent_context import AgentContextManager, get_agent_context + +__all__ = [ + # I18n services + "I18nService", + "get_i18n_service", + # Agent context services + "AgentContextManager", + "get_agent_context", + # Note: For asset services, import directly from valuecell.services.assets +] diff --git a/python/valuecell/services/agent_context.py b/python/valuecell/server/services/agent_context.py similarity index 91% rename from python/valuecell/services/agent_context.py rename to python/valuecell/server/services/agent_context.py index a82ba59b7..0908c66b2 100644 --- a/python/valuecell/services/agent_context.py +++ b/python/valuecell/server/services/agent_context.py @@ -5,9 +5,8 @@ import threading from contextlib import contextmanager -from ..api.i18n_api import get_i18n_api from ..services.i18n_service import get_i18n_service -from ..api.schemas import AgentI18nContext +from ..api.schemas.i18n import AgentI18nContextData class AgentContextManager: @@ -15,19 +14,23 @@ class AgentContextManager: def __init__(self): """Initialize agent context manager.""" - self.i18n_api = get_i18n_api() self.i18n_service = get_i18n_service() self._local = threading.local() - - def set_user_context(self, user_id: str, session_id: Optional[str] = None): + self._user_contexts = {} # Store user contexts locally + + def set_user_context( + self, + user_id: str, + session_id: Optional[str] = None, + language: str = "en-US", + timezone: str = "UTC", + ): """Set current user context for the agent.""" - user_context = self.i18n_api.get_user_context(user_id) - # Store in thread local storage self._local.user_id = user_id self._local.session_id = session_id - self._local.language = user_context.get("language", "en-US") - self._local.timezone = user_context.get("timezone", "UTC") + self._local.language = language + self._local.timezone = timezone # Update i18n service self.i18n_service.set_language(self._local.language) @@ -49,9 +52,9 @@ def get_current_timezone(self) -> str: """Get current user's timezone.""" return getattr(self._local, "timezone", "UTC") - def get_i18n_context(self) -> AgentI18nContext: + def get_i18n_context(self) -> AgentI18nContextData: """Get complete i18n context for agent.""" - return AgentI18nContext( + return AgentI18nContextData( language=self.get_current_language(), timezone=self.get_current_timezone(), currency_symbol=self.i18n_service._i18n_config.get_currency_symbol(), @@ -154,7 +157,7 @@ def get_current_user_id() -> Optional[str]: return get_agent_context().get_current_user_id() -def get_i18n_context() -> AgentI18nContext: +def get_i18n_context() -> AgentI18nContextData: """Get i18n context (convenience function).""" return get_agent_context().get_i18n_context() diff --git a/python/valuecell/services/assets/__init__.py b/python/valuecell/server/services/assets/__init__.py similarity index 100% rename from python/valuecell/services/assets/__init__.py rename to python/valuecell/server/services/assets/__init__.py diff --git a/python/valuecell/services/assets/asset_service.py b/python/valuecell/server/services/assets/asset_service.py similarity index 100% rename from python/valuecell/services/assets/asset_service.py rename to python/valuecell/server/services/assets/asset_service.py diff --git a/python/valuecell/services/i18n_service.py b/python/valuecell/server/services/i18n_service.py similarity index 92% rename from python/valuecell/services/i18n_service.py rename to python/valuecell/server/services/i18n_service.py index e3cdd2cff..d62b87f99 100644 --- a/python/valuecell/services/i18n_service.py +++ b/python/valuecell/server/services/i18n_service.py @@ -7,7 +7,7 @@ from ..config.settings import get_settings from ..config.i18n import get_i18n_config -from ..core.constants import SUPPORTED_LANGUAGE_CODES, DEFAULT_LANGUAGE +from ...core.constants import SUPPORTED_LANGUAGE_CODES, DEFAULT_LANGUAGE class TranslationManager: @@ -229,7 +229,7 @@ def get_supported_languages(self) -> List[tuple]: Returns: List of (code, name) tuples """ - from ..core.constants import SUPPORTED_LANGUAGES + from ...core.constants import SUPPORTED_LANGUAGES return SUPPORTED_LANGUAGES @@ -242,7 +242,7 @@ def get_language_name(self, language_code: str) -> str: Returns: Display name or code if not found """ - from ..core.constants import SUPPORTED_LANGUAGES + from ...core.constants import SUPPORTED_LANGUAGES for code, name in SUPPORTED_LANGUAGES: if code == language_code: @@ -269,13 +269,18 @@ def to_dict(self) -> Dict[str, Any]: """Get current i18n configuration as dictionary. Returns: - Dictionary with i18n configuration + Dictionary with i18n configuration matching I18nConfigData schema """ + config_dict = self._i18n_config.to_dict() return { - "current_language": self.get_current_language(), - "current_timezone": self.get_current_timezone(), - "supported_languages": self.get_supported_languages(), - "config": self._i18n_config.to_dict(), + "language": config_dict["language"], + "timezone": config_dict["timezone"], + "date_format": config_dict["date_format"], + "time_format": config_dict["time_format"], + "datetime_format": config_dict["datetime_format"], + "currency_symbol": config_dict["currency_symbol"], + "number_format": config_dict["number_format"], + "is_rtl": config_dict["is_rtl"], } diff --git a/python/valuecell/services/__init__.py b/python/valuecell/services/__init__.py deleted file mode 100644 index b5f1f42cd..000000000 --- a/python/valuecell/services/__init__.py +++ /dev/null @@ -1,23 +0,0 @@ -"""ValueCell Services Module. - -This module provides high-level service layers for various business operations -including asset management, internationalization, and agent context management. -""" - -# Asset service (import directly from .assets to avoid circular imports) - -# I18n service -from .i18n_service import I18nService, get_i18n_service - -# Agent context service -from .agent_context import AgentContextManager, get_agent_context - -__all__ = [ - # I18n services - "I18nService", - "get_i18n_service", - # Agent context services - "AgentContextManager", - "get_agent_context", - # Note: For asset services, import directly from valuecell.services.assets -] diff --git a/python/valuecell/utils/i18n_utils.py b/python/valuecell/utils/i18n_utils.py index e3cd657bf..c0f628a9c 100644 --- a/python/valuecell/utils/i18n_utils.py +++ b/python/valuecell/utils/i18n_utils.py @@ -13,7 +13,7 @@ DEFAULT_TIMEZONE, SUPPORTED_LANGUAGES, ) -from ..services.i18n_service import get_i18n_service +from ..server.services.i18n_service import get_i18n_service def detect_browser_language(accept_language_header: str) -> str: