From c7383ebac8c1ea3028d0e16d7b78c32a06ca71da Mon Sep 17 00:00:00 2001 From: zhonghao lu Date: Mon, 15 Sep 2025 15:02:21 +0800 Subject: [PATCH 01/10] refactor: integrate server into valuecell package --- python/valuecell/server/__init__.py | 0 python/valuecell/server/api/__init__.py | 0 python/valuecell/server/api/app.py | 60 ++++ .../valuecell/server/api/routers/__init__.py | 5 + python/valuecell/server/api/routers/agents.py | 93 ++++++ python/valuecell/server/api/routers/assets.py | 71 ++++ python/valuecell/server/api/routers/health.py | 41 +++ python/valuecell/server/api/routers/i18n.py | 288 ++++++++++++++++ .../valuecell/server/api/schemas/__init__.py | 65 ++++ python/valuecell/server/api/schemas/agents.py | 104 ++++++ python/valuecell/server/api/schemas/assets.py | 125 +++++++ python/valuecell/server/api/schemas/common.py | 28 ++ python/valuecell/server/api/schemas/health.py | 22 ++ python/valuecell/server/api/schemas/i18n.py | 149 +++++++++ python/valuecell/server/config/__init__.py | 0 python/valuecell/server/config/database.py | 40 +++ python/valuecell/server/config/logging.py | 64 ++++ python/valuecell/server/config/settings.py | 84 +++++ python/valuecell/server/db/__init__.py | 0 .../db/migrations/001_initial_schema.py | 132 ++++++++ .../server/db/migrations/__init__.py | 0 python/valuecell/server/db/models/__init__.py | 12 + python/valuecell/server/db/models/agent.py | 80 +++++ python/valuecell/server/db/models/asset.py | 130 ++++++++ python/valuecell/server/db/models/base.py | 11 + .../server/db/repositories/__init__.py | 9 + .../db/repositories/agent_repository.py | 161 +++++++++ .../db/repositories/asset_repository.py | 232 +++++++++++++ python/valuecell/server/main.py | 22 ++ python/valuecell/server/requirements.txt | 53 +++ python/valuecell/server/services/__init__.py | 11 + .../server/services/agents/__init__.py | 0 .../server/services/agents/agent_service.py | 137 ++++++++ .../server/services/assets/__init__.py | 0 .../server/services/assets/asset_service.py | 160 +++++++++ .../server/services/auth/__init__.py | 0 .../server/services/i18n/__init__.py | 5 + .../server/services/i18n/i18n_service.py | 311 ++++++++++++++++++ 38 files changed, 2705 insertions(+) create mode 100644 python/valuecell/server/__init__.py create mode 100644 python/valuecell/server/api/__init__.py create mode 100644 python/valuecell/server/api/app.py create mode 100644 python/valuecell/server/api/routers/__init__.py create mode 100644 python/valuecell/server/api/routers/agents.py create mode 100644 python/valuecell/server/api/routers/assets.py create mode 100644 python/valuecell/server/api/routers/health.py create mode 100644 python/valuecell/server/api/routers/i18n.py create mode 100644 python/valuecell/server/api/schemas/__init__.py create mode 100644 python/valuecell/server/api/schemas/agents.py create mode 100644 python/valuecell/server/api/schemas/assets.py create mode 100644 python/valuecell/server/api/schemas/common.py create mode 100644 python/valuecell/server/api/schemas/health.py create mode 100644 python/valuecell/server/api/schemas/i18n.py create mode 100644 python/valuecell/server/config/__init__.py create mode 100644 python/valuecell/server/config/database.py create mode 100644 python/valuecell/server/config/logging.py create mode 100644 python/valuecell/server/config/settings.py create mode 100644 python/valuecell/server/db/__init__.py create mode 100644 python/valuecell/server/db/migrations/001_initial_schema.py create mode 100644 python/valuecell/server/db/migrations/__init__.py create mode 100644 python/valuecell/server/db/models/__init__.py create mode 100644 python/valuecell/server/db/models/agent.py create mode 100644 python/valuecell/server/db/models/asset.py create mode 100644 python/valuecell/server/db/models/base.py create mode 100644 python/valuecell/server/db/repositories/__init__.py create mode 100644 python/valuecell/server/db/repositories/agent_repository.py create mode 100644 python/valuecell/server/db/repositories/asset_repository.py create mode 100644 python/valuecell/server/main.py create mode 100644 python/valuecell/server/requirements.txt create mode 100644 python/valuecell/server/services/__init__.py create mode 100644 python/valuecell/server/services/agents/__init__.py create mode 100644 python/valuecell/server/services/agents/agent_service.py create mode 100644 python/valuecell/server/services/assets/__init__.py create mode 100644 python/valuecell/server/services/assets/asset_service.py create mode 100644 python/valuecell/server/services/auth/__init__.py create mode 100644 python/valuecell/server/services/i18n/__init__.py create mode 100644 python/valuecell/server/services/i18n/i18n_service.py diff --git a/python/valuecell/server/__init__.py b/python/valuecell/server/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/python/valuecell/server/api/__init__.py b/python/valuecell/server/api/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/python/valuecell/server/api/app.py b/python/valuecell/server/api/app.py new file mode 100644 index 000000000..f3a92b2c5 --- /dev/null +++ b/python/valuecell/server/api/app.py @@ -0,0 +1,60 @@ +"""FastAPI application factory for ValueCell Server.""" + +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from contextlib import asynccontextmanager + +from ..config.settings import get_settings +from .routers import health, agents, assets, i18n + + +def create_app() -> FastAPI: + """Create and configure FastAPI application.""" + settings = get_settings() + + @asynccontextmanager + async def lifespan(app: FastAPI): + # Startup + print(f"ValueCell Server starting up on {settings.API_HOST}:{settings.API_PORT}...") + yield + # Shutdown + print("ValueCell Server shutting down...") + + app = FastAPI( + title="ValueCell Server API", + description="A community-driven, multi-agent platform for financial applications", + version=settings.APP_VERSION, + lifespan=lifespan, + docs_url="/docs" if settings.API_DEBUG else None, + redoc_url="/redoc" if settings.API_DEBUG else None, + ) + + # Add middleware + _add_middleware(app, settings) + + # Add routes + _add_routes(app) + + return app + + +def _add_middleware(app: FastAPI, settings) -> None: + """Add middleware to the application.""" + # CORS middleware + app.add_middleware( + CORSMiddleware, + allow_origins=settings.CORS_ORIGINS, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) + + # Custom logging middleware removed + + +def _add_routes(app: FastAPI) -> None: + """Add routes to the application.""" + app.include_router(health.router, prefix="/health", tags=["health"]) + app.include_router(agents.router, prefix="/api/v1/agents", tags=["agents"]) + app.include_router(assets.router, prefix="/api/v1/assets", tags=["assets"]) + app.include_router(i18n.router, prefix="/api/v1", tags=["i18n"]) \ No newline at end of file diff --git a/python/valuecell/server/api/routers/__init__.py b/python/valuecell/server/api/routers/__init__.py new file mode 100644 index 000000000..7d41cfdcc --- /dev/null +++ b/python/valuecell/server/api/routers/__init__.py @@ -0,0 +1,5 @@ +"""API routers for ValueCell Server.""" + +from . import health, agents, assets, i18n + +__all__ = ["health", "agents", "assets", "i18n"] \ No newline at end of file diff --git a/python/valuecell/server/api/routers/agents.py b/python/valuecell/server/api/routers/agents.py new file mode 100644 index 000000000..f28cc748d --- /dev/null +++ b/python/valuecell/server/api/routers/agents.py @@ -0,0 +1,93 @@ +"""Agents router for ValueCell Server.""" + +from typing import List +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session + +from ...config.database import get_db +from ...services.agents.agent_service import AgentService +from ..schemas.agents import ( + AgentResponse, + AgentCreateRequest, + AgentUpdateRequest, + AgentExecutionRequest, + AgentExecutionResponse, +) + +router = APIRouter() + + +@router.get("/", response_model=List[AgentResponse]) +async def list_agents(db: Session = Depends(get_db)): + """List all available agents.""" + agent_service = AgentService(db) + return await agent_service.list_agents() + + +@router.get("/{agent_id}", response_model=AgentResponse) +async def get_agent(agent_id: str, db: Session = Depends(get_db)): + """Get agent by ID.""" + agent_service = AgentService(db) + agent = await agent_service.get_agent(agent_id) + if not agent: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Agent not found" + ) + return agent + + +@router.post("/", response_model=AgentResponse, status_code=status.HTTP_201_CREATED) +async def create_agent( + agent_data: AgentCreateRequest, + db: Session = Depends(get_db) +): + """Create a new agent.""" + agent_service = AgentService(db) + return await agent_service.create_agent(agent_data) + + +@router.put("/{agent_id}", response_model=AgentResponse) +async def update_agent( + agent_id: str, + agent_data: AgentUpdateRequest, + db: Session = Depends(get_db) +): + """Update an existing agent.""" + agent_service = AgentService(db) + agent = await agent_service.update_agent(agent_id, agent_data) + if not agent: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Agent not found" + ) + return agent + + +@router.delete("/{agent_id}", status_code=status.HTTP_204_NO_CONTENT) +async def delete_agent(agent_id: str, db: Session = Depends(get_db)): + """Delete an agent.""" + agent_service = AgentService(db) + success = await agent_service.delete_agent(agent_id) + if not success: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Agent not found" + ) + + +@router.post("/{agent_id}/execute", response_model=AgentExecutionResponse) +async def execute_agent( + agent_id: str, + execution_request: AgentExecutionRequest, + db: Session = Depends(get_db) +): + """Execute an agent with given input.""" + agent_service = AgentService(db) + result = await agent_service.execute_agent(agent_id, execution_request) + if not result: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Agent not found" + ) + return result \ No newline at end of file diff --git a/python/valuecell/server/api/routers/assets.py b/python/valuecell/server/api/routers/assets.py new file mode 100644 index 000000000..916bfb478 --- /dev/null +++ b/python/valuecell/server/api/routers/assets.py @@ -0,0 +1,71 @@ +"""Assets router for ValueCell Server.""" + +from typing import List, Optional +from fastapi import APIRouter, Depends, HTTPException, Query, status +from sqlalchemy.orm import Session + +from ...config.database import get_db +from ...services.assets.asset_service import AssetService +from ..schemas.assets import ( + AssetResponse, + AssetPriceResponse, + AssetSearchRequest, +) + +router = APIRouter() + + +@router.get("/search", response_model=List[AssetResponse]) +async def search_assets( + query: str = Query(..., description="Search query for assets"), + limit: int = Query(10, ge=1, le=100, description="Maximum number of results"), + db: Session = Depends(get_db) +): + """Search for assets by symbol or name.""" + asset_service = AssetService(db) + return await asset_service.search_assets(query, limit) + + +@router.get("/{symbol}", response_model=AssetResponse) +async def get_asset( + symbol: str, + db: Session = Depends(get_db) +): + """Get asset information by symbol.""" + asset_service = AssetService(db) + asset = await asset_service.get_asset(symbol) + if not asset: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Asset with symbol '{symbol}' not found" + ) + return asset + + +@router.get("/{symbol}/price", response_model=AssetPriceResponse) +async def get_asset_price( + symbol: str, + period: Optional[str] = Query("1d", description="Time period (1d, 1w, 1m, 3m, 6m, 1y)"), + db: Session = Depends(get_db) +): + """Get current and historical price data for an asset.""" + asset_service = AssetService(db) + price_data = await asset_service.get_asset_price(symbol, period) + if not price_data: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Price data for asset '{symbol}' not found" + ) + return price_data + + +@router.get("/", response_model=List[AssetResponse]) +async def list_assets( + skip: int = Query(0, ge=0, description="Number of records to skip"), + limit: int = Query(50, ge=1, le=100, description="Maximum number of results"), + asset_type: Optional[str] = Query(None, description="Filter by asset type (stock, crypto, forex)"), + db: Session = Depends(get_db) +): + """List assets with pagination and filtering.""" + asset_service = AssetService(db) + return await asset_service.list_assets(skip, limit, asset_type) \ No newline at end of file diff --git a/python/valuecell/server/api/routers/health.py b/python/valuecell/server/api/routers/health.py new file mode 100644 index 000000000..acae3bdb6 --- /dev/null +++ b/python/valuecell/server/api/routers/health.py @@ -0,0 +1,41 @@ +"""Health check router for ValueCell Server.""" + +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session +from ...config.database import get_db +from ...config.settings import get_settings +from ..schemas.health import HealthResponse + +router = APIRouter() + + +@router.get("/", response_model=HealthResponse) +async def health_check(db: Session = Depends(get_db)): + """Health check endpoint.""" + settings = get_settings() + + # Test database connection + try: + db.execute("SELECT 1") + db_status = "healthy" + except Exception as e: + db_status = f"unhealthy: {str(e)}" + + return HealthResponse( + status="healthy", + version=settings.APP_VERSION, + environment=settings.APP_ENVIRONMENT, + database=db_status, + ) + + +@router.get("/ready") +async def readiness_check(): + """Readiness check endpoint.""" + return {"status": "ready"} + + +@router.get("/live") +async def liveness_check(): + """Liveness check endpoint.""" + return {"status": "alive"} \ No newline at end of file diff --git a/python/valuecell/server/api/routers/i18n.py b/python/valuecell/server/api/routers/i18n.py new file mode 100644 index 000000000..137f7f33b --- /dev/null +++ b/python/valuecell/server/api/routers/i18n.py @@ -0,0 +1,288 @@ +"""I18n router for ValueCell Server.""" + +from typing import Dict, Any, Optional +from fastapi import APIRouter, HTTPException, Header, Depends +from datetime import datetime +from sqlalchemy.orm import Session + +from ..schemas.common import SuccessResponse +from ..schemas.i18n import ( + LanguageRequest, + TimezoneRequest, + LanguageDetectionRequest, + TranslationRequest, + DateTimeFormatRequest, + NumberFormatRequest, + CurrencyFormatRequest, + UserI18nSettingsRequest, + AgentI18nContext, + I18nConfigResponse, + SupportedLanguagesResponse, + TimezonesResponse, +) +from ...config.database import get_db +from ...services.i18n.i18n_service import I18nService +from ...config.logging import get_logger + +logger = get_logger(__name__) + +router = APIRouter(prefix="/i18n", tags=["i18n"]) + + +# Dependency to get i18n service +def get_i18n_service(db: Session = Depends(get_db)) -> I18nService: + """Get i18n service instance.""" + return I18nService(db) + + +@router.get("/config", response_model=SuccessResponse) +async def get_config( + user_id: Optional[str] = Header(None, alias="X-User-ID"), + session_id: Optional[str] = Header(None, alias="X-Session-ID"), + i18n_service: I18nService = Depends(get_i18n_service), +) -> SuccessResponse: + """Get i18n configuration for user.""" + try: + config = await i18n_service.get_user_config(user_id) + return SuccessResponse( + message="I18n configuration retrieved successfully", + data=config + ) + except Exception as e: + logger.error(f"Error getting i18n config: {e}", exc_info=True) + raise HTTPException(status_code=500, detail="Internal server error") + + +@router.get("/languages", response_model=SuccessResponse) +async def get_supported_languages( + i18n_service: I18nService = Depends(get_i18n_service), +) -> SuccessResponse: + """Get supported languages.""" + try: + languages = await i18n_service.get_supported_languages() + return SuccessResponse( + message="Supported languages retrieved successfully", + data=languages + ) + except Exception as e: + logger.error(f"Error getting supported languages: {e}", exc_info=True) + raise HTTPException(status_code=500, detail="Internal server error") + + +@router.post("/language", response_model=SuccessResponse) +async def set_language( + request: LanguageRequest, + user_id: Optional[str] = Header(None, alias="X-User-ID"), + i18n_service: I18nService = Depends(get_i18n_service), +) -> SuccessResponse: + """Set user language preference.""" + try: + result = await i18n_service.set_user_language(user_id, request.language) + return SuccessResponse( + message=f"Language set to {request.language}", + data=result + ) + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + except Exception as e: + logger.error(f"Error setting language: {e}", exc_info=True) + raise HTTPException(status_code=500, detail="Internal server error") + + +@router.get("/timezones", response_model=SuccessResponse) +async def get_timezones( + i18n_service: I18nService = Depends(get_i18n_service), +) -> SuccessResponse: + """Get supported timezones.""" + try: + timezones = await i18n_service.get_supported_timezones() + return SuccessResponse( + message="Supported timezones retrieved successfully", + data=timezones + ) + except Exception as e: + logger.error(f"Error getting timezones: {e}", exc_info=True) + raise HTTPException(status_code=500, detail="Internal server error") + + +@router.post("/timezone", response_model=SuccessResponse) +async def set_timezone( + request: TimezoneRequest, + user_id: Optional[str] = Header(None, alias="X-User-ID"), + i18n_service: I18nService = Depends(get_i18n_service), +) -> SuccessResponse: + """Set user timezone preference.""" + try: + result = await i18n_service.set_user_timezone(user_id, request.timezone) + return SuccessResponse( + message=f"Timezone set to {request.timezone}", + data=result + ) + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + except Exception as e: + logger.error(f"Error setting timezone: {e}", exc_info=True) + raise HTTPException(status_code=500, detail="Internal server error") + + +@router.post("/detect-language", response_model=SuccessResponse) +async def detect_language( + request: LanguageDetectionRequest, + i18n_service: I18nService = Depends(get_i18n_service), +) -> SuccessResponse: + """Detect language from Accept-Language header.""" + try: + detected = await i18n_service.detect_language(request.accept_language) + return SuccessResponse( + message="Language detected successfully", + data=detected + ) + except Exception as e: + logger.error(f"Error detecting language: {e}", exc_info=True) + raise HTTPException(status_code=500, detail="Internal server error") + + +@router.post("/translate", response_model=SuccessResponse) +async def translate( + request: TranslationRequest, + i18n_service: I18nService = Depends(get_i18n_service), +) -> SuccessResponse: + """Translate a key to target language.""" + try: + translation = await i18n_service.translate( + key=request.key, + language=request.language, + variables=request.variables + ) + return SuccessResponse( + message="Translation retrieved successfully", + data={"translation": translation} + ) + except Exception as e: + logger.error(f"Error translating: {e}", exc_info=True) + raise HTTPException(status_code=500, detail="Internal server error") + + +@router.post("/format/datetime", response_model=SuccessResponse) +async def format_datetime( + request: DateTimeFormatRequest, + user_id: Optional[str] = Header(None, alias="X-User-ID"), + i18n_service: I18nService = Depends(get_i18n_service), +) -> SuccessResponse: + """Format datetime according to user preferences.""" + try: + formatted = await i18n_service.format_datetime( + datetime_str=request.datetime, + format_type=request.format_type, + user_id=user_id + ) + return SuccessResponse( + message="DateTime formatted successfully", + data={"formatted": formatted} + ) + except Exception as e: + logger.error(f"Error formatting datetime: {e}", exc_info=True) + raise HTTPException(status_code=500, detail="Internal server error") + + +@router.post("/format/number", response_model=SuccessResponse) +async def format_number( + request: NumberFormatRequest, + user_id: Optional[str] = Header(None, alias="X-User-ID"), + i18n_service: I18nService = Depends(get_i18n_service), +) -> SuccessResponse: + """Format number according to user preferences.""" + try: + formatted = await i18n_service.format_number( + number=request.number, + decimal_places=request.decimal_places, + user_id=user_id + ) + return SuccessResponse( + message="Number formatted successfully", + data={"formatted": formatted} + ) + except Exception as e: + logger.error(f"Error formatting number: {e}", exc_info=True) + raise HTTPException(status_code=500, detail="Internal server error") + + +@router.post("/format/currency", response_model=SuccessResponse) +async def format_currency( + request: CurrencyFormatRequest, + user_id: Optional[str] = Header(None, alias="X-User-ID"), + i18n_service: I18nService = Depends(get_i18n_service), +) -> SuccessResponse: + """Format currency according to user preferences.""" + try: + formatted = await i18n_service.format_currency( + amount=request.amount, + decimal_places=request.decimal_places, + user_id=user_id + ) + return SuccessResponse( + message="Currency formatted successfully", + data={"formatted": formatted} + ) + except Exception as e: + logger.error(f"Error formatting currency: {e}", exc_info=True) + raise HTTPException(status_code=500, detail="Internal server error") + + +@router.get("/user/settings", response_model=SuccessResponse) +async def get_user_settings( + user_id: str = Header(..., alias="X-User-ID"), + i18n_service: I18nService = Depends(get_i18n_service), +) -> SuccessResponse: + """Get user i18n settings.""" + try: + settings = await i18n_service.get_user_settings(user_id) + return SuccessResponse( + message="User settings retrieved successfully", + data=settings + ) + except Exception as e: + logger.error(f"Error getting user settings: {e}", exc_info=True) + raise HTTPException(status_code=500, detail="Internal server error") + + +@router.put("/user/settings", response_model=SuccessResponse) +async def update_user_settings( + request: UserI18nSettingsRequest, + user_id: str = Header(..., alias="X-User-ID"), + i18n_service: I18nService = Depends(get_i18n_service), +) -> SuccessResponse: + """Update user i18n settings.""" + try: + settings = await i18n_service.update_user_settings( + user_id=user_id, + language=request.language, + timezone=request.timezone + ) + return SuccessResponse( + message="User settings updated successfully", + data=settings + ) + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + except Exception as e: + logger.error(f"Error updating user settings: {e}", exc_info=True) + raise HTTPException(status_code=500, detail="Internal server error") + + +@router.get("/agent/context", response_model=SuccessResponse) +async def get_agent_context( + user_id: Optional[str] = Header(None, alias="X-User-ID"), + session_id: Optional[str] = Header(None, alias="X-Session-ID"), + i18n_service: I18nService = Depends(get_i18n_service), +) -> SuccessResponse: + """Get i18n context for agent execution.""" + try: + context = await i18n_service.get_agent_context(user_id, session_id) + return SuccessResponse( + message="Agent context retrieved successfully", + data=context + ) + except Exception as e: + logger.error(f"Error getting agent context: {e}", exc_info=True) + raise HTTPException(status_code=500, detail="Internal server error") \ No newline at end of file diff --git a/python/valuecell/server/api/schemas/__init__.py b/python/valuecell/server/api/schemas/__init__.py new file mode 100644 index 000000000..5e19255ec --- /dev/null +++ b/python/valuecell/server/api/schemas/__init__.py @@ -0,0 +1,65 @@ +"""API schemas for ValueCell Server.""" + +from .common import BaseResponse, ErrorResponse, SuccessResponse +from .health import HealthResponse +from .agents import ( + AgentResponse, + AgentCreateRequest, + AgentUpdateRequest, + AgentExecutionRequest, + AgentExecutionResponse, +) +from .assets import ( + AssetResponse, + AssetPriceResponse, + AssetSearchRequest, + PricePoint, +) +from .i18n import ( + I18nConfigResponse, + SupportedLanguage, + SupportedLanguagesResponse, + TimezoneInfo, + TimezonesResponse, + LanguageRequest, + TimezoneRequest, + LanguageDetectionRequest, + TranslationRequest, + DateTimeFormatRequest, + NumberFormatRequest, + CurrencyFormatRequest, + UserI18nSettings, + UserI18nSettingsRequest, + AgentI18nContext, +) + +__all__ = [ + "BaseResponse", + "ErrorResponse", + "SuccessResponse", + "HealthResponse", + "AgentResponse", + "AgentCreateRequest", + "AgentUpdateRequest", + "AgentExecutionRequest", + "AgentExecutionResponse", + "AssetResponse", + "AssetPriceResponse", + "AssetSearchRequest", + "PricePoint", + "I18nConfigResponse", + "SupportedLanguage", + "SupportedLanguagesResponse", + "TimezoneInfo", + "TimezonesResponse", + "LanguageRequest", + "TimezoneRequest", + "LanguageDetectionRequest", + "TranslationRequest", + "DateTimeFormatRequest", + "NumberFormatRequest", + "CurrencyFormatRequest", + "UserI18nSettings", + "UserI18nSettingsRequest", + "AgentI18nContext", +] \ No newline at end of file diff --git a/python/valuecell/server/api/schemas/agents.py b/python/valuecell/server/api/schemas/agents.py new file mode 100644 index 000000000..38afb052c --- /dev/null +++ b/python/valuecell/server/api/schemas/agents.py @@ -0,0 +1,104 @@ +"""Agent schemas for ValueCell Server.""" + +from typing import Dict, Any, Optional, List +from datetime import datetime +from pydantic import BaseModel, Field + + +class AgentBase(BaseModel): + """Base agent model.""" + + name: str = Field(..., description="Agent name") + description: str = Field(..., description="Agent description") + agent_type: str = Field(..., description="Type of agent (e.g., 'sec_13f', 'calculator')") + config: Dict[str, Any] = Field(default_factory=dict, description="Agent configuration") + is_active: bool = Field(True, description="Whether the agent is active") + + +class AgentCreateRequest(AgentBase): + """Request model for creating an agent.""" + pass + + +class AgentUpdateRequest(BaseModel): + """Request model for updating an agent.""" + + name: Optional[str] = Field(None, description="Agent name") + description: Optional[str] = Field(None, description="Agent description") + config: Optional[Dict[str, Any]] = Field(None, description="Agent configuration") + is_active: Optional[bool] = Field(None, description="Whether the agent is active") + + +class AgentResponse(AgentBase): + """Response model for agent data.""" + + id: str = Field(..., description="Agent ID") + created_at: datetime = Field(..., description="Creation timestamp") + updated_at: datetime = Field(..., description="Last update timestamp") + + class Config: + from_attributes = True + schema_extra = { + "example": { + "id": "agent_123", + "name": "SEC 13F Analyzer", + "description": "Analyzes SEC 13F filings for institutional holdings", + "agent_type": "sec_13f", + "config": { + "model": "gpt-4", + "temperature": 0.7 + }, + "is_active": True, + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:00:00Z" + } + } + + +class AgentExecutionRequest(BaseModel): + """Request model for agent execution.""" + + input_data: Dict[str, Any] = Field(..., description="Input data for the agent") + parameters: Optional[Dict[str, Any]] = Field(default_factory=dict, description="Execution parameters") + + class Config: + schema_extra = { + "example": { + "input_data": { + "query": "Analyze Berkshire Hathaway's latest 13F filing", + "ticker": "BRK.A" + }, + "parameters": { + "streaming": True, + "timeout": 300 + } + } + } + + +class AgentExecutionResponse(BaseModel): + """Response model for agent execution.""" + + execution_id: str = Field(..., description="Execution ID") + agent_id: str = Field(..., description="Agent ID") + status: str = Field(..., description="Execution status") + result: Optional[Dict[str, Any]] = Field(None, description="Execution result") + error: Optional[str] = Field(None, description="Error message if execution failed") + started_at: datetime = Field(..., description="Execution start time") + completed_at: Optional[datetime] = Field(None, description="Execution completion time") + + class Config: + schema_extra = { + "example": { + "execution_id": "exec_456", + "agent_id": "agent_123", + "status": "completed", + "result": { + "content": "Analysis of Berkshire Hathaway's 13F filing...", + "is_task_complete": True + }, + "error": None, + "started_at": "2024-01-01T00:00:00Z", + "completed_at": "2024-01-01T00:05:00Z" + } + } \ No newline at end of file diff --git a/python/valuecell/server/api/schemas/assets.py b/python/valuecell/server/api/schemas/assets.py new file mode 100644 index 000000000..38d9acd1d --- /dev/null +++ b/python/valuecell/server/api/schemas/assets.py @@ -0,0 +1,125 @@ +"""Asset schemas for ValueCell Server.""" + +from typing import List, Optional, Dict, Any +from datetime import datetime +from decimal import Decimal +from pydantic import BaseModel, Field + + +class AssetBase(BaseModel): + """Base asset model.""" + + symbol: str = Field(..., description="Asset symbol (e.g., AAPL, BTC)") + name: str = Field(..., description="Asset name") + asset_type: str = Field(..., description="Asset type (stock, crypto, forex, etc.)") + exchange: Optional[str] = Field(None, description="Exchange where the asset is traded") + currency: str = Field("USD", description="Base currency") + + +class AssetResponse(AssetBase): + """Response model for asset data.""" + + id: str = Field(..., description="Asset ID") + market_cap: Optional[Decimal] = Field(None, description="Market capitalization") + sector: Optional[str] = Field(None, description="Asset sector") + industry: Optional[str] = Field(None, description="Asset industry") + description: Optional[str] = Field(None, description="Asset description") + is_active: bool = Field(True, description="Whether the asset is actively traded") + created_at: datetime = Field(..., description="Creation timestamp") + updated_at: datetime = Field(..., description="Last update timestamp") + + class Config: + from_attributes = True + schema_extra = { + "example": { + "id": "asset_123", + "symbol": "AAPL", + "name": "Apple Inc.", + "asset_type": "stock", + "exchange": "NASDAQ", + "currency": "USD", + "market_cap": "3000000000000", + "sector": "Technology", + "industry": "Consumer Electronics", + "description": "Apple Inc. designs, manufactures, and markets smartphones...", + "is_active": True, + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:00:00Z" + } + } + + +class PricePoint(BaseModel): + """Single price point model.""" + + timestamp: datetime = Field(..., description="Price timestamp") + open: Decimal = Field(..., description="Opening price") + high: Decimal = Field(..., description="Highest price") + low: Decimal = Field(..., description="Lowest price") + close: Decimal = Field(..., description="Closing price") + volume: Optional[int] = Field(None, description="Trading volume") + + class Config: + schema_extra = { + "example": { + "timestamp": "2024-01-01T00:00:00Z", + "open": "150.00", + "high": "155.00", + "low": "149.00", + "close": "154.00", + "volume": 1000000 + } + } + + +class AssetPriceResponse(BaseModel): + """Response model for asset price data.""" + + symbol: str = Field(..., description="Asset symbol") + current_price: Decimal = Field(..., description="Current price") + price_change: Decimal = Field(..., description="Price change from previous close") + price_change_percent: Decimal = Field(..., description="Price change percentage") + period: str = Field(..., description="Time period for historical data") + historical_data: List[PricePoint] = Field(..., description="Historical price data") + last_updated: datetime = Field(..., description="Last update timestamp") + + class Config: + schema_extra = { + "example": { + "symbol": "AAPL", + "current_price": "154.00", + "price_change": "2.50", + "price_change_percent": "1.65", + "period": "1d", + "historical_data": [ + { + "timestamp": "2024-01-01T00:00:00Z", + "open": "150.00", + "high": "155.00", + "low": "149.00", + "close": "154.00", + "volume": 1000000 + } + ], + "last_updated": "2024-01-01T16:00:00Z" + } + } + + +class AssetSearchRequest(BaseModel): + """Request model for asset search.""" + + query: str = Field(..., description="Search query") + asset_types: Optional[List[str]] = Field(None, description="Filter by asset types") + exchanges: Optional[List[str]] = Field(None, description="Filter by exchanges") + limit: int = Field(10, ge=1, le=100, description="Maximum number of results") + + class Config: + schema_extra = { + "example": { + "query": "Apple", + "asset_types": ["stock"], + "exchanges": ["NASDAQ"], + "limit": 10 + } + } \ No newline at end of file diff --git a/python/valuecell/server/api/schemas/common.py b/python/valuecell/server/api/schemas/common.py new file mode 100644 index 000000000..05c822a1b --- /dev/null +++ b/python/valuecell/server/api/schemas/common.py @@ -0,0 +1,28 @@ +"""Common schemas for ValueCell Server.""" + +from typing import Dict, Any, Optional +from pydantic import BaseModel + + +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] \ No newline at end of file diff --git a/python/valuecell/server/api/schemas/health.py b/python/valuecell/server/api/schemas/health.py new file mode 100644 index 000000000..32c7812e3 --- /dev/null +++ b/python/valuecell/server/api/schemas/health.py @@ -0,0 +1,22 @@ +"""Health check schemas for ValueCell Server.""" + +from pydantic import BaseModel + + +class HealthResponse(BaseModel): + """Health check response model.""" + + status: str + version: str + environment: str + database: str + + class Config: + schema_extra = { + "example": { + "status": "healthy", + "version": "0.1.0", + "environment": "development", + "database": "healthy" + } + } \ No newline at end of file diff --git a/python/valuecell/server/api/schemas/i18n.py b/python/valuecell/server/api/schemas/i18n.py new file mode 100644 index 000000000..17deb0676 --- /dev/null +++ b/python/valuecell/server/api/schemas/i18n.py @@ -0,0 +1,149 @@ +"""I18n schemas for ValueCell Server.""" + +from typing import Dict, Any, List, Optional +from datetime import datetime +from pydantic import BaseModel, Field, validator + + +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 setting request.""" + + language: str = Field(..., description="Language code to set") + + @validator("language") + def validate_language(cls, v): + """Validate language code.""" + # TODO: Add proper validation logic + return v + + +class TimezoneRequest(BaseModel): + """Timezone setting 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 format request.""" + + datetime: str = Field(..., description="ISO datetime string") + format_type: str = Field( + "datetime", description="Format type: date, time, or datetime" + ) + + +class NumberFormatRequest(BaseModel): + """Number format request.""" + + number: float = Field(..., description="Number to format") + decimal_places: int = Field(2, description="Number of decimal places") + + +class CurrencyFormatRequest(BaseModel): + """Currency format request.""" + + amount: float = Field(..., description="Amount to format") + decimal_places: int = Field(2, description="Number of decimal places") + + +class UserI18nSettings(BaseModel): + """User i18n settings.""" + + 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): + """Validate language code.""" + # TODO: Add proper validation logic + 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): + """Validate language code.""" + if v is not None: + # TODO: Add proper validation logic + pass + return v + + +class AgentI18nContext(BaseModel): + """Agent i18n context.""" + + 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 \ No newline at end of file diff --git a/python/valuecell/server/config/__init__.py b/python/valuecell/server/config/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/python/valuecell/server/config/database.py b/python/valuecell/server/config/database.py new file mode 100644 index 000000000..94f73beba --- /dev/null +++ b/python/valuecell/server/config/database.py @@ -0,0 +1,40 @@ +"""Database configuration for ValueCell Server.""" + +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker +from .settings import get_settings + +settings = get_settings() + +# Create database engine +engine = create_engine( + settings.DATABASE_URL, + echo=settings.DB_ECHO, + pool_pre_ping=True, +) + +# Create session factory +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +# Create base class for models +Base = declarative_base() + + +def get_db(): + """Get database session.""" + db = SessionLocal() + try: + yield db + finally: + db.close() + + +def create_tables(): + """Create all tables.""" + Base.metadata.create_all(bind=engine) + + +def drop_tables(): + """Drop all tables.""" + Base.metadata.drop_all(bind=engine) \ No newline at end of file diff --git a/python/valuecell/server/config/logging.py b/python/valuecell/server/config/logging.py new file mode 100644 index 000000000..b1df542c9 --- /dev/null +++ b/python/valuecell/server/config/logging.py @@ -0,0 +1,64 @@ +"""Logging configuration for ValueCell Server.""" + +import logging +import logging.config +from pathlib import Path +from .settings import get_settings + +settings = get_settings() + + +def setup_logging(): + """Setup logging configuration.""" + log_config = { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "default": { + "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s", + }, + "json": { + "format": '{"timestamp": "%(asctime)s", "name": "%(name)s", "level": "%(levelname)s", "message": "%(message)s"}', + }, + }, + "handlers": { + "console": { + "class": "logging.StreamHandler", + "level": settings.LOG_LEVEL, + "formatter": settings.LOG_FORMAT if settings.LOG_FORMAT in ["default", "json"] else "default", + "stream": "ext://sys.stdout", + }, + "file": { + "class": "logging.handlers.RotatingFileHandler", + "level": settings.LOG_LEVEL, + "formatter": settings.LOG_FORMAT if settings.LOG_FORMAT in ["default", "json"] else "default", + "filename": settings.LOGS_DIR / "valuecell.log", + "maxBytes": 10485760, # 10MB + "backupCount": 5, + }, + }, + "loggers": { + "": { + "level": settings.LOG_LEVEL, + "handlers": ["console", "file"], + "propagate": False, + }, + "uvicorn": { + "level": "INFO", + "handlers": ["console"], + "propagate": False, + }, + "sqlalchemy": { + "level": "WARNING", + "handlers": ["console"], + "propagate": False, + }, + }, + } + + logging.config.dictConfig(log_config) + + +def get_logger(name: str) -> logging.Logger: + """Get logger instance.""" + return logging.getLogger(name) \ No newline at end of file diff --git a/python/valuecell/server/config/settings.py b/python/valuecell/server/config/settings.py new file mode 100644 index 000000000..b8ebbb94c --- /dev/null +++ b/python/valuecell/server/config/settings.py @@ -0,0 +1,84 @@ +"""Settings configuration for ValueCell Server.""" + +import os +from typing import List, Optional +from pathlib import Path +from functools import lru_cache + + +class Settings: + """Server configuration settings.""" + + def __init__(self): + """Initialize settings from environment variables.""" + # Application Configuration + self.APP_NAME = os.getenv("APP_NAME", "ValueCell Server") + 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" + + # CORS Configuration + cors_origins = os.getenv("CORS_ORIGINS", "*") + self.CORS_ORIGINS = cors_origins.split(",") if cors_origins != "*" else ["*"] + + # Database Configuration + self.DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./valuecell.db") + self.DB_ECHO = os.getenv("DB_ECHO", "false").lower() == "true" + + # Redis Configuration + self.REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379") + + # Security Configuration + self.SECRET_KEY = os.getenv("SECRET_KEY", "your-secret-key-here") + self.ACCESS_TOKEN_EXPIRE_MINUTES = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "30")) + + # Logging Configuration + self.LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO") + self.LOG_FORMAT = os.getenv("LOG_FORMAT", "json") + + # File Paths + self.BASE_DIR = Path(__file__).parent.parent.parent + self.LOGS_DIR = self.BASE_DIR / "logs" + self.LOGS_DIR.mkdir(exist_ok=True) + + # Agent Configuration + self.AGENT_TIMEOUT = int(os.getenv("AGENT_TIMEOUT", "300")) # 5 minutes + self.MAX_CONCURRENT_AGENTS = int(os.getenv("MAX_CONCURRENT_AGENTS", "10")) + + # External APIs + self.OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") + self.FINNHUB_API_KEY = os.getenv("FINNHUB_API_KEY") + self.ALPHA_VANTAGE_API_KEY = os.getenv("ALPHA_VANTAGE_API_KEY") + + @property + def is_development(self) -> bool: + """Check if running in development mode.""" + return self.APP_ENVIRONMENT == "development" + + @property + def is_production(self) -> bool: + """Check if running in production mode.""" + return self.APP_ENVIRONMENT == "production" + + def get_database_config(self) -> dict: + """Get database configuration.""" + return { + "url": self.DATABASE_URL, + "echo": self.DB_ECHO, + } + + def get_redis_config(self) -> dict: + """Get Redis configuration.""" + return { + "url": self.REDIS_URL, + } + + +@lru_cache() +def get_settings() -> Settings: + """Get cached settings instance.""" + return Settings() \ No newline at end of file diff --git a/python/valuecell/server/db/__init__.py b/python/valuecell/server/db/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/python/valuecell/server/db/migrations/001_initial_schema.py b/python/valuecell/server/db/migrations/001_initial_schema.py new file mode 100644 index 000000000..055335de5 --- /dev/null +++ b/python/valuecell/server/db/migrations/001_initial_schema.py @@ -0,0 +1,132 @@ +"""Initial database schema migration for ValueCell Server.""" + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers +revision = '001' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + """Create initial database schema.""" + + # Create agents table + op.create_table( + 'agents', + sa.Column('id', sa.String(), nullable=False), + sa.Column('name', sa.String(length=255), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('agent_type', sa.String(length=100), nullable=False), + sa.Column('config', sa.JSON(), nullable=False), + sa.Column('is_active', sa.Boolean(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('created_by', sa.String(length=255), nullable=True), + sa.Column('updated_by', sa.String(length=255), nullable=True), + sa.Column('execution_count', sa.Integer(), nullable=False), + sa.Column('last_executed_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('average_execution_time', sa.Float(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + + # Create indexes for agents table + op.create_index(op.f('ix_agents_id'), 'agents', ['id'], unique=False) + op.create_index(op.f('ix_agents_name'), 'agents', ['name'], unique=False) + op.create_index(op.f('ix_agents_agent_type'), 'agents', ['agent_type'], unique=False) + op.create_index(op.f('ix_agents_is_active'), 'agents', ['is_active'], unique=False) + + # Create assets table + op.create_table( + 'assets', + sa.Column('id', sa.String(), nullable=False), + sa.Column('symbol', sa.String(length=20), nullable=False), + sa.Column('name', sa.String(length=255), nullable=False), + sa.Column('asset_type', sa.String(length=50), nullable=False), + sa.Column('exchange', sa.String(length=100), nullable=True), + sa.Column('currency', sa.String(length=10), nullable=False), + sa.Column('market_cap', sa.DECIMAL(precision=20, scale=2), nullable=True), + sa.Column('sector', sa.String(length=100), nullable=True), + sa.Column('industry', sa.String(length=100), nullable=True), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('data_source', sa.String(length=100), nullable=True), + sa.Column('last_price_update', sa.DateTime(timezone=True), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('symbol') + ) + + # Create indexes for assets table + op.create_index(op.f('ix_assets_id'), 'assets', ['id'], unique=False) + op.create_index(op.f('ix_assets_symbol'), 'assets', ['symbol'], unique=True) + op.create_index(op.f('ix_assets_name'), 'assets', ['name'], unique=False) + op.create_index(op.f('ix_assets_asset_type'), 'assets', ['asset_type'], unique=False) + op.create_index(op.f('ix_assets_exchange'), 'assets', ['exchange'], unique=False) + op.create_index(op.f('ix_assets_sector'), 'assets', ['sector'], unique=False) + op.create_index(op.f('ix_assets_is_active'), 'assets', ['is_active'], unique=False) + + # Create asset_prices table + op.create_table( + 'asset_prices', + sa.Column('id', sa.String(), nullable=False), + sa.Column('asset_id', sa.String(), nullable=False), + sa.Column('symbol', sa.String(length=20), nullable=False), + sa.Column('timestamp', sa.DateTime(timezone=True), nullable=False), + sa.Column('open_price', sa.DECIMAL(precision=20, scale=8), nullable=False), + sa.Column('high_price', sa.DECIMAL(precision=20, scale=8), nullable=False), + sa.Column('low_price', sa.DECIMAL(precision=20, scale=8), nullable=False), + sa.Column('close_price', sa.DECIMAL(precision=20, scale=8), nullable=False), + sa.Column('volume', sa.Integer(), nullable=True), + sa.Column('adjusted_close', sa.DECIMAL(precision=20, scale=8), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('data_source', sa.String(length=100), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + + # Create indexes for asset_prices table + op.create_index(op.f('ix_asset_prices_id'), 'asset_prices', ['id'], unique=False) + op.create_index(op.f('ix_asset_prices_asset_id'), 'asset_prices', ['asset_id'], unique=False) + op.create_index(op.f('ix_asset_prices_symbol'), 'asset_prices', ['symbol'], unique=False) + op.create_index(op.f('ix_asset_prices_timestamp'), 'asset_prices', ['timestamp'], unique=False) + + # Create composite index for efficient price queries + op.create_index( + 'ix_asset_prices_symbol_timestamp', + 'asset_prices', + ['symbol', 'timestamp'], + unique=False + ) + + +def downgrade(): + """Drop all tables.""" + + # Drop indexes first + op.drop_index('ix_asset_prices_symbol_timestamp', table_name='asset_prices') + op.drop_index(op.f('ix_asset_prices_timestamp'), table_name='asset_prices') + op.drop_index(op.f('ix_asset_prices_symbol'), table_name='asset_prices') + op.drop_index(op.f('ix_asset_prices_asset_id'), table_name='asset_prices') + op.drop_index(op.f('ix_asset_prices_id'), table_name='asset_prices') + + op.drop_index(op.f('ix_assets_is_active'), table_name='assets') + op.drop_index(op.f('ix_assets_sector'), table_name='assets') + op.drop_index(op.f('ix_assets_exchange'), table_name='assets') + op.drop_index(op.f('ix_assets_asset_type'), table_name='assets') + op.drop_index(op.f('ix_assets_name'), table_name='assets') + op.drop_index(op.f('ix_assets_symbol'), table_name='assets') + op.drop_index(op.f('ix_assets_id'), table_name='assets') + + op.drop_index(op.f('ix_agents_is_active'), table_name='agents') + op.drop_index(op.f('ix_agents_agent_type'), table_name='agents') + op.drop_index(op.f('ix_agents_name'), table_name='agents') + op.drop_index(op.f('ix_agents_id'), table_name='agents') + + # Drop tables + op.drop_table('asset_prices') + op.drop_table('assets') + op.drop_table('agents') \ No newline at end of file diff --git a/python/valuecell/server/db/migrations/__init__.py b/python/valuecell/server/db/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/python/valuecell/server/db/models/__init__.py b/python/valuecell/server/db/models/__init__.py new file mode 100644 index 000000000..f39ff5b79 --- /dev/null +++ b/python/valuecell/server/db/models/__init__.py @@ -0,0 +1,12 @@ +"""Database models for ValueCell Server.""" + +from .base import Base +from .agent import Agent +from .asset import Asset, AssetPrice + +__all__ = [ + "Base", + "Agent", + "Asset", + "AssetPrice", +] \ No newline at end of file diff --git a/python/valuecell/server/db/models/agent.py b/python/valuecell/server/db/models/agent.py new file mode 100644 index 000000000..67f873ed0 --- /dev/null +++ b/python/valuecell/server/db/models/agent.py @@ -0,0 +1,80 @@ +"""Agent model for ValueCell Server.""" + +from typing import Dict, Any, Optional +from datetime import datetime +from sqlalchemy import Column, String, Text, Boolean, DateTime, JSON, Integer, Float +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.sql import func +import uuid +from .base import Base + + +class Agent(Base): + """Agent model for storing agent configurations and metadata.""" + + __tablename__ = "agents" + + id = Column( + String, + primary_key=True, + default=lambda: str(uuid.uuid4()), + index=True + ) + name = Column(String(255), nullable=False, index=True) + description = Column(Text, nullable=True) + agent_type = Column(String(100), nullable=False, index=True) + config = Column(JSON, nullable=False, default=dict) + is_active = Column(Boolean, default=True, nullable=False, index=True) + + # Metadata + created_at = Column( + DateTime(timezone=True), + server_default=func.now(), + nullable=False + ) + updated_at = Column( + DateTime(timezone=True), + server_default=func.now(), + onupdate=func.now(), + nullable=False + ) + created_by = Column(String(255), nullable=True) + updated_by = Column(String(255), nullable=True) + + # Performance tracking + execution_count = Column(Integer, default=0, nullable=False) + last_executed_at = Column(DateTime(timezone=True), nullable=True) + average_execution_time = Column(Float, nullable=True) + + def __repr__(self) -> str: + return f"" + + def to_dict(self) -> Dict[str, Any]: + """Convert agent to dictionary.""" + return { + "id": self.id, + "name": self.name, + "description": self.description, + "agent_type": self.agent_type, + "config": self.config, + "is_active": self.is_active, + "created_at": self.created_at.isoformat() if self.created_at else None, + "updated_at": self.updated_at.isoformat() if self.updated_at else None, + "created_by": self.created_by, + "updated_by": self.updated_by, + "execution_count": self.execution_count, + "last_executed_at": self.last_executed_at.isoformat() if self.last_executed_at else None, + "average_execution_time": self.average_execution_time, + } + + def update_execution_stats(self, execution_time: float) -> None: + """Update execution statistics.""" + self.execution_count += 1 + self.last_executed_at = datetime.utcnow() + + if self.average_execution_time is None: + self.average_execution_time = execution_time + else: + # Calculate running average + total_time = self.average_execution_time * (self.execution_count - 1) + self.average_execution_time = (total_time + execution_time) / self.execution_count \ No newline at end of file diff --git a/python/valuecell/server/db/models/asset.py b/python/valuecell/server/db/models/asset.py new file mode 100644 index 000000000..258127d30 --- /dev/null +++ b/python/valuecell/server/db/models/asset.py @@ -0,0 +1,130 @@ +"""Asset model for ValueCell Server.""" + +from typing import Dict, Any, Optional +from datetime import datetime +from decimal import Decimal +from sqlalchemy import Column, String, Text, Boolean, DateTime, DECIMAL, Integer +from sqlalchemy.sql import func +import uuid +from .base import Base + + +class Asset(Base): + """Asset model for storing financial instruments and market data.""" + + __tablename__ = "assets" + + id = Column( + String, + primary_key=True, + default=lambda: str(uuid.uuid4()), + index=True + ) + symbol = Column(String(20), nullable=False, unique=True, index=True) + name = Column(String(255), nullable=False, index=True) + asset_type = Column(String(50), nullable=False, index=True) # stock, crypto, forex, etc. + exchange = Column(String(100), nullable=True, index=True) + currency = Column(String(10), nullable=False, default="USD") + + # Market data + market_cap = Column(DECIMAL(20, 2), nullable=True) + sector = Column(String(100), nullable=True, index=True) + industry = Column(String(100), nullable=True, index=True) + description = Column(Text, nullable=True) + + # Status + is_active = Column(Boolean, default=True, nullable=False, index=True) + + # Metadata + created_at = Column( + DateTime(timezone=True), + server_default=func.now(), + nullable=False + ) + updated_at = Column( + DateTime(timezone=True), + server_default=func.now(), + onupdate=func.now(), + nullable=False + ) + + # Data source tracking + data_source = Column(String(100), nullable=True) # yahoo, alpha_vantage, etc. + last_price_update = Column(DateTime(timezone=True), nullable=True) + + def __repr__(self) -> str: + return f"" + + def to_dict(self) -> Dict[str, Any]: + """Convert asset to dictionary.""" + return { + "id": self.id, + "symbol": self.symbol, + "name": self.name, + "asset_type": self.asset_type, + "exchange": self.exchange, + "currency": self.currency, + "market_cap": str(self.market_cap) if self.market_cap else None, + "sector": self.sector, + "industry": self.industry, + "description": self.description, + "is_active": self.is_active, + "created_at": self.created_at.isoformat() if self.created_at else None, + "updated_at": self.updated_at.isoformat() if self.updated_at else None, + "data_source": self.data_source, + "last_price_update": self.last_price_update.isoformat() if self.last_price_update else None, + } + + +class AssetPrice(Base): + """Asset price model for storing historical price data.""" + + __tablename__ = "asset_prices" + + id = Column( + String, + primary_key=True, + default=lambda: str(uuid.uuid4()), + index=True + ) + asset_id = Column(String, nullable=False, index=True) + symbol = Column(String(20), nullable=False, index=True) + + # Price data + timestamp = Column(DateTime(timezone=True), nullable=False, index=True) + open_price = Column(DECIMAL(20, 8), nullable=False) + high_price = Column(DECIMAL(20, 8), nullable=False) + low_price = Column(DECIMAL(20, 8), nullable=False) + close_price = Column(DECIMAL(20, 8), nullable=False) + volume = Column(Integer, nullable=True) + + # Adjusted prices (for stocks) + adjusted_close = Column(DECIMAL(20, 8), nullable=True) + + # Metadata + created_at = Column( + DateTime(timezone=True), + server_default=func.now(), + nullable=False + ) + data_source = Column(String(100), nullable=True) + + def __repr__(self) -> str: + return f"" + + def to_dict(self) -> Dict[str, Any]: + """Convert asset price to dictionary.""" + return { + "id": self.id, + "asset_id": self.asset_id, + "symbol": self.symbol, + "timestamp": self.timestamp.isoformat() if self.timestamp else None, + "open": str(self.open_price), + "high": str(self.high_price), + "low": str(self.low_price), + "close": str(self.close_price), + "volume": self.volume, + "adjusted_close": str(self.adjusted_close) if self.adjusted_close else None, + "created_at": self.created_at.isoformat() if self.created_at else None, + "data_source": self.data_source, + } \ No newline at end of file diff --git a/python/valuecell/server/db/models/base.py b/python/valuecell/server/db/models/base.py new file mode 100644 index 000000000..59b61b0c7 --- /dev/null +++ b/python/valuecell/server/db/models/base.py @@ -0,0 +1,11 @@ +"""Base model for ValueCell Server.""" + +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import DeclarativeBase + +# Create the base class for all models +Base = declarative_base() + +# Alternative approach using modern SQLAlchemy 2.0 style +# class Base(DeclarativeBase): +# pass \ No newline at end of file diff --git a/python/valuecell/server/db/repositories/__init__.py b/python/valuecell/server/db/repositories/__init__.py new file mode 100644 index 000000000..4d3eb93d8 --- /dev/null +++ b/python/valuecell/server/db/repositories/__init__.py @@ -0,0 +1,9 @@ +"""Database repositories for ValueCell Server.""" + +from .agent_repository import AgentRepository +from .asset_repository import AssetRepository + +__all__ = [ + "AgentRepository", + "AssetRepository", +] \ No newline at end of file diff --git a/python/valuecell/server/db/repositories/agent_repository.py b/python/valuecell/server/db/repositories/agent_repository.py new file mode 100644 index 000000000..99c1a988b --- /dev/null +++ b/python/valuecell/server/db/repositories/agent_repository.py @@ -0,0 +1,161 @@ +"""Agent repository for ValueCell Server.""" + +from typing import List, Optional, Dict, Any +from sqlalchemy.orm import Session +from sqlalchemy import and_, or_ +from ..models.agent import Agent +from ...config.logging import get_logger + +logger = get_logger(__name__) + + +class AgentRepository: + """Repository for agent data access.""" + + def __init__(self, db: Session): + """Initialize agent repository.""" + self.db = db + + async def get_agent(self, agent_id: str) -> Optional[Agent]: + """Get agent by ID.""" + try: + return self.db.query(Agent).filter(Agent.id == agent_id).first() + except Exception as e: + logger.error(f"Error getting agent {agent_id}", exc_info=True) + return None + + async def get_agent_by_name(self, name: str) -> Optional[Agent]: + """Get agent by name.""" + try: + return self.db.query(Agent).filter(Agent.name == name).first() + except Exception as e: + logger.error(f"Error getting agent by name {name}", exc_info=True) + return None + + async def list_agents( + self, + skip: int = 0, + limit: int = 100, + filters: Optional[Dict[str, Any]] = None, + order_by: str = "created_at", + order_desc: bool = False, + ) -> List[Agent]: + """List agents with optional filtering and pagination.""" + try: + query = self.db.query(Agent) + + # Apply filters + if filters: + for key, value in filters.items(): + if hasattr(Agent, key): + if isinstance(value, list): + query = query.filter(getattr(Agent, key).in_(value)) + else: + query = query.filter(getattr(Agent, key) == value) + + # Apply ordering + if hasattr(Agent, order_by): + order_column = getattr(Agent, order_by) + if order_desc: + query = query.order_by(order_column.desc()) + else: + query = query.order_by(order_column) + + # Apply pagination + return query.offset(skip).limit(limit).all() + + except Exception as e: + logger.error(f"Error listing agents", exc_info=True) + return [] + + async def create_agent(self, agent: Agent) -> Agent: + """Create a new agent.""" + try: + self.db.add(agent) + self.db.commit() + self.db.refresh(agent) + logger.info(f"Created agent: {agent.id}") + return agent + except Exception as e: + self.db.rollback() + logger.error(f"Error creating agent", exc_info=True) + raise + + async def update_agent(self, agent: Agent) -> Agent: + """Update an existing agent.""" + try: + self.db.commit() + self.db.refresh(agent) + logger.info(f"Updated agent: {agent.id}") + return agent + except Exception as e: + self.db.rollback() + logger.error(f"Error updating agent {agent.id}", exc_info=True) + raise + + async def delete_agent(self, agent_id: str) -> bool: + """Delete an agent.""" + try: + agent = await self.get_agent(agent_id) + if agent: + self.db.delete(agent) + self.db.commit() + logger.info(f"Deleted agent: {agent_id}") + return True + return False + except Exception as e: + self.db.rollback() + logger.error(f"Error deleting agent {agent_id}", exc_info=True) + return False + + async def search_agents( + self, + query: str, + filters: Optional[Dict[str, Any]] = None, + limit: int = 10 + ) -> List[Agent]: + """Search agents by name or description.""" + try: + db_query = self.db.query(Agent) + + # Text search + search_filter = or_( + Agent.name.ilike(f"%{query}%"), + Agent.description.ilike(f"%{query}%") + ) + db_query = db_query.filter(search_filter) + + # Apply additional filters + if filters: + for key, value in filters.items(): + if hasattr(Agent, key): + if isinstance(value, list): + db_query = db_query.filter(getattr(Agent, key).in_(value)) + else: + db_query = db_query.filter(getattr(Agent, key) == value) + + return db_query.limit(limit).all() + + except Exception as e: + logger.error(f"Error searching agents with query: {query}", exc_info=True) + return [] + + async def count_agents(self, filters: Optional[Dict[str, Any]] = None) -> int: + """Count agents with optional filtering.""" + try: + query = self.db.query(Agent) + + # Apply filters + if filters: + for key, value in filters.items(): + if hasattr(Agent, key): + if isinstance(value, list): + query = query.filter(getattr(Agent, key).in_(value)) + else: + query = query.filter(getattr(Agent, key) == value) + + return query.count() + + except Exception as e: + logger.error(f"Error counting agents", exc_info=True) + return 0 \ No newline at end of file diff --git a/python/valuecell/server/db/repositories/asset_repository.py b/python/valuecell/server/db/repositories/asset_repository.py new file mode 100644 index 000000000..7cc12cc4e --- /dev/null +++ b/python/valuecell/server/db/repositories/asset_repository.py @@ -0,0 +1,232 @@ +"""Asset repository for ValueCell Server.""" + +from typing import List, Optional, Dict, Any +from datetime import datetime +from sqlalchemy.orm import Session +from sqlalchemy import and_, or_, desc +from ..models.asset import Asset, AssetPrice +from ...config.logging import get_logger + +logger = get_logger(__name__) + + +class AssetRepository: + """Repository for asset data access.""" + + def __init__(self, db: Session): + """Initialize asset repository.""" + self.db = db + + async def get_asset(self, asset_id: str) -> Optional[Asset]: + """Get asset by ID.""" + try: + return self.db.query(Asset).filter(Asset.id == asset_id).first() + except Exception as e: + logger.error(f"Error getting asset {asset_id}", exc_info=True) + return None + + async def get_asset_by_symbol(self, symbol: str) -> Optional[Asset]: + """Get asset by symbol.""" + try: + return self.db.query(Asset).filter(Asset.symbol == symbol.upper()).first() + except Exception as e: + logger.error(f"Error getting asset by symbol {symbol}", exc_info=True) + return None + + async def list_assets( + self, + skip: int = 0, + limit: int = 100, + filters: Optional[Dict[str, Any]] = None, + order_by: str = "created_at", + order_desc: bool = False, + ) -> List[Asset]: + """List assets with optional filtering and pagination.""" + try: + query = self.db.query(Asset) + + # Apply filters + if filters: + for key, value in filters.items(): + if hasattr(Asset, key): + if isinstance(value, list): + query = query.filter(getattr(Asset, key).in_(value)) + else: + query = query.filter(getattr(Asset, key) == value) + + # Apply ordering + if hasattr(Asset, order_by): + order_column = getattr(Asset, order_by) + if order_desc: + query = query.order_by(order_column.desc()) + else: + query = query.order_by(order_column) + + # Apply pagination + return query.offset(skip).limit(limit).all() + + except Exception as e: + logger.error(f"Error listing assets", exc_info=True) + return [] + + async def search_assets( + self, + query: str, + filters: Optional[Dict[str, Any]] = None, + limit: int = 10 + ) -> List[Asset]: + """Search assets by symbol, name, or description.""" + try: + db_query = self.db.query(Asset) + + # Text search + search_filter = or_( + Asset.symbol.ilike(f"%{query}%"), + Asset.name.ilike(f"%{query}%"), + Asset.description.ilike(f"%{query}%") + ) + db_query = db_query.filter(search_filter) + + # Apply additional filters + if filters: + for key, value in filters.items(): + if hasattr(Asset, key): + if isinstance(value, list): + db_query = db_query.filter(getattr(Asset, key).in_(value)) + else: + db_query = db_query.filter(getattr(Asset, key) == value) + + return db_query.limit(limit).all() + + except Exception as e: + logger.error(f"Error searching assets with query: {query}", exc_info=True) + return [] + + async def create_asset(self, asset: Asset) -> Asset: + """Create a new asset.""" + try: + self.db.add(asset) + self.db.commit() + self.db.refresh(asset) + logger.info(f"Created asset: {asset.id}") + return asset + except Exception as e: + self.db.rollback() + logger.error(f"Error creating asset", exc_info=True) + raise + + async def update_asset(self, asset: Asset) -> Asset: + """Update an existing asset.""" + try: + self.db.commit() + self.db.refresh(asset) + logger.info(f"Updated asset: {asset.id}") + return asset + except Exception as e: + self.db.rollback() + logger.error(f"Error updating asset {asset.id}", exc_info=True) + raise + + async def delete_asset(self, asset_id: str) -> bool: + """Delete an asset.""" + try: + asset = await self.get_asset(asset_id) + if asset: + self.db.delete(asset) + self.db.commit() + logger.info(f"Deleted asset: {asset_id}") + return True + return False + except Exception as e: + self.db.rollback() + logger.error(f"Error deleting asset {asset_id}", exc_info=True) + return False + + # Asset Price methods + + async def get_latest_price(self, symbol: str) -> Optional[AssetPrice]: + """Get the latest price for an asset.""" + try: + return ( + self.db.query(AssetPrice) + .filter(AssetPrice.symbol == symbol.upper()) + .order_by(desc(AssetPrice.timestamp)) + .first() + ) + except Exception as e: + logger.error(f"Error getting latest price for {symbol}", exc_info=True) + return None + + async def get_price_history( + self, + symbol: str, + start_date: Optional[datetime] = None, + end_date: Optional[datetime] = None, + limit: int = 100 + ) -> List[AssetPrice]: + """Get price history for an asset.""" + try: + query = ( + self.db.query(AssetPrice) + .filter(AssetPrice.symbol == symbol.upper()) + ) + + if start_date: + query = query.filter(AssetPrice.timestamp >= start_date) + if end_date: + query = query.filter(AssetPrice.timestamp <= end_date) + + return ( + query + .order_by(desc(AssetPrice.timestamp)) + .limit(limit) + .all() + ) + + except Exception as e: + logger.error(f"Error getting price history for {symbol}", exc_info=True) + return [] + + async def create_price(self, price: AssetPrice) -> AssetPrice: + """Create a new price record.""" + try: + self.db.add(price) + self.db.commit() + self.db.refresh(price) + return price + except Exception as e: + self.db.rollback() + logger.error(f"Error creating price record", exc_info=True) + raise + + async def bulk_create_prices(self, prices: List[AssetPrice]) -> bool: + """Bulk create price records.""" + try: + self.db.add_all(prices) + self.db.commit() + logger.info(f"Created {len(prices)} price records") + return True + except Exception as e: + self.db.rollback() + logger.error(f"Error bulk creating price records", exc_info=True) + return False + + async def count_assets(self, filters: Optional[Dict[str, Any]] = None) -> int: + """Count assets with optional filtering.""" + try: + query = self.db.query(Asset) + + # Apply filters + if filters: + for key, value in filters.items(): + if hasattr(Asset, key): + if isinstance(value, list): + query = query.filter(getattr(Asset, key).in_(value)) + else: + query = query.filter(getattr(Asset, key) == value) + + return query.count() + + except Exception as e: + logger.error(f"Error counting assets", exc_info=True) + return 0 \ No newline at end of file diff --git a/python/valuecell/server/main.py b/python/valuecell/server/main.py new file mode 100644 index 000000000..23469b69c --- /dev/null +++ b/python/valuecell/server/main.py @@ -0,0 +1,22 @@ +"""Main entry point for ValueCell Server Backend.""" + +import uvicorn +from .api.app import create_app +from .config.settings import get_settings + + +def main(): + """Start the server.""" + settings = get_settings() + app = create_app() + + uvicorn.run( + app, + host=settings.API_HOST, + port=settings.API_PORT, + reload=settings.API_DEBUG, + ) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/python/valuecell/server/requirements.txt b/python/valuecell/server/requirements.txt new file mode 100644 index 000000000..a0ed9e8de --- /dev/null +++ b/python/valuecell/server/requirements.txt @@ -0,0 +1,53 @@ +# FastAPI and web framework +fastapi==0.104.1 +uvicorn[standard]==0.24.0 +pydantic==2.5.0 +pydantic-settings==2.1.0 + +# Database +sqlalchemy==2.0.23 +alembic==1.13.1 +psycopg2-binary==2.9.9 + +# Redis for caching +redis==5.0.1 +hiredis==2.2.3 + +# HTTP client +httpx==0.25.2 +aiohttp==3.9.1 + +# Authentication and security +python-jose[cryptography]==3.3.0 +passlib[bcrypt]==1.7.4 +python-multipart==0.0.6 + +# Logging and monitoring +structlog==23.2.0 + +# Configuration +python-dotenv==1.0.0 + +# Data processing +pandas==2.1.4 +numpy==1.26.2 + +# Financial data +yfinance==0.2.28 +alpha-vantage==2.3.1 + +# Testing +pytest==7.4.3 +pytest-asyncio==0.21.1 +pytest-cov==4.1.0 +httpx==0.25.2 # for testing FastAPI + +# Development tools +black==23.11.0 +isort==5.12.0 +flake8==6.1.0 +mypy==1.7.1 + +# Utilities +click==8.1.7 +rich==13.7.0 \ No newline at end of file diff --git a/python/valuecell/server/services/__init__.py b/python/valuecell/server/services/__init__.py new file mode 100644 index 000000000..75bf3b085 --- /dev/null +++ b/python/valuecell/server/services/__init__.py @@ -0,0 +1,11 @@ +"""Services for ValueCell Server.""" + +from .agents.agent_service import AgentService +from .assets.asset_service import AssetService +from .i18n.i18n_service import I18nService + +__all__ = [ + "AgentService", + "AssetService", + "I18nService", +] \ No newline at end of file diff --git a/python/valuecell/server/services/agents/__init__.py b/python/valuecell/server/services/agents/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/python/valuecell/server/services/agents/agent_service.py b/python/valuecell/server/services/agents/agent_service.py new file mode 100644 index 000000000..fe19d7549 --- /dev/null +++ b/python/valuecell/server/services/agents/agent_service.py @@ -0,0 +1,137 @@ +"""Agent service for ValueCell Server.""" + +import time +from typing import List, Optional, Dict, Any +from sqlalchemy.orm import Session +from ...db.repositories.agent_repository import AgentRepository +from ...db.models.agent import Agent +from ...api.schemas.agents import ( + AgentCreateRequest, + AgentUpdateRequest, + AgentExecutionRequest, + AgentExecutionResponse, +) +from ...config.logging import get_logger + +logger = get_logger(__name__) + + +class AgentService: + """Service for managing agents.""" + + def __init__(self, db: Session): + """Initialize agent service.""" + self.db = db + self.repository = AgentRepository(db) + + async def list_agents( + self, + skip: int = 0, + limit: int = 100, + agent_type: Optional[str] = None, + is_active: Optional[bool] = None, + ) -> List[Agent]: + """List agents with optional filtering.""" + logger.info(f"Listing agents: skip={skip}, limit={limit}, type={agent_type}") + + filters = {} + if agent_type: + filters["agent_type"] = agent_type + if is_active is not None: + filters["is_active"] = is_active + + return await self.repository.list_agents( + skip=skip, + limit=limit, + filters=filters + ) + + async def get_agent(self, agent_id: str) -> Optional[Agent]: + """Get agent by ID.""" + logger.info(f"Getting agent: {agent_id}") + return await self.repository.get_agent(agent_id) + + async def create_agent(self, agent_data: AgentCreateRequest) -> Agent: + """Create a new agent.""" + logger.info(f"Creating agent: {agent_data.name}") + + agent = Agent( + name=agent_data.name, + description=agent_data.description, + agent_type=agent_data.agent_type, + config=agent_data.config, + is_active=agent_data.is_active, + ) + + return await self.repository.create_agent(agent) + + async def update_agent( + self, + agent_id: str, + agent_data: AgentUpdateRequest + ) -> Optional[Agent]: + """Update an existing agent.""" + logger.info(f"Updating agent: {agent_id}") + + agent = await self.repository.get_agent(agent_id) + if not agent: + return None + + # Update fields + update_data = agent_data.dict(exclude_unset=True) + for field, value in update_data.items(): + setattr(agent, field, value) + + return await self.repository.update_agent(agent) + + async def delete_agent(self, agent_id: str) -> bool: + """Delete an agent.""" + logger.info(f"Deleting agent: {agent_id}") + return await self.repository.delete_agent(agent_id) + + async def execute_agent( + self, + agent_id: str, + execution_request: AgentExecutionRequest + ) -> AgentExecutionResponse: + """Execute an agent with given parameters.""" + logger.info(f"Executing agent: {agent_id}") + + agent = await self.repository.get_agent(agent_id) + if not agent: + raise ValueError(f"Agent {agent_id} not found") + + if not agent.is_active: + raise ValueError(f"Agent {agent_id} is not active") + + try: + # TODO: Implement actual agent execution logic + # This would integrate with the existing agent execution framework + + # For now, return a mock response + result = { + "status": "completed", + "message": f"Agent {agent.name} executed successfully", + "data": execution_request.parameters, + } + + logger.info(f"Agent execution completed: {agent_id}") + + return AgentExecutionResponse( + agent_id=agent_id, + execution_id=f"exec_{agent_id}_{int(time.time())}", + status="completed", + result=result, + error=None, + ) + + except Exception as e: + logger.error(f"Agent execution failed: {agent_id}", exc_info=True) + + return AgentExecutionResponse( + agent_id=agent_id, + execution_id=f"exec_{agent_id}_{int(time.time())}", + status="failed", + result=None, + error=str(e), + ) \ No newline at end of file diff --git a/python/valuecell/server/services/assets/__init__.py b/python/valuecell/server/services/assets/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/python/valuecell/server/services/assets/asset_service.py b/python/valuecell/server/services/assets/asset_service.py new file mode 100644 index 000000000..69946b8bb --- /dev/null +++ b/python/valuecell/server/services/assets/asset_service.py @@ -0,0 +1,160 @@ +"""Asset service for ValueCell Server.""" + +from typing import List, Optional, Dict, Any +from datetime import datetime, timedelta +from decimal import Decimal +from sqlalchemy.orm import Session +from ...db.repositories.asset_repository import AssetRepository +from ...db.models.asset import Asset +from ...api.schemas.assets import ( + AssetSearchRequest, + AssetResponse, + AssetPriceResponse, + PricePoint, +) +from ...config.logging import get_logger + +logger = get_logger(__name__) + + +class AssetService: + """Service for managing assets and market data.""" + + def __init__(self, db: Session): + """Initialize asset service.""" + self.db = db + self.repository = AssetRepository(db) + + async def search_assets( + self, + search_request: AssetSearchRequest + ) -> List[Asset]: + """Search assets based on query and filters.""" + logger.info(f"Searching assets: {search_request.query}") + + filters = {} + if search_request.asset_types: + filters["asset_type"] = search_request.asset_types + if search_request.exchanges: + filters["exchange"] = search_request.exchanges + + return await self.repository.search_assets( + query=search_request.query, + filters=filters, + limit=search_request.limit + ) + + async def get_asset(self, asset_id: str) -> Optional[Asset]: + """Get asset by ID.""" + logger.info(f"Getting asset: {asset_id}") + return await self.repository.get_asset(asset_id) + + async def get_asset_by_symbol(self, symbol: str) -> Optional[Asset]: + """Get asset by symbol.""" + logger.info(f"Getting asset by symbol: {symbol}") + return await self.repository.get_asset_by_symbol(symbol) + + async def list_assets( + self, + skip: int = 0, + limit: int = 100, + asset_type: Optional[str] = None, + exchange: Optional[str] = None, + is_active: Optional[bool] = None, + ) -> List[Asset]: + """List assets with optional filtering.""" + logger.info(f"Listing assets: skip={skip}, limit={limit}") + + filters = {} + if asset_type: + filters["asset_type"] = asset_type + if exchange: + filters["exchange"] = exchange + if is_active is not None: + filters["is_active"] = is_active + + return await self.repository.list_assets( + skip=skip, + limit=limit, + filters=filters + ) + + async def get_asset_price( + self, + symbol: str, + period: str = "1d", + interval: str = "1h" + ) -> Optional[AssetPriceResponse]: + """Get current and historical price data for an asset.""" + logger.info(f"Getting price data for: {symbol}") + + asset = await self.get_asset_by_symbol(symbol) + if not asset: + return None + + try: + # TODO: Integrate with actual market data provider + # This would connect to APIs like Alpha Vantage, Yahoo Finance, etc. + + # For now, return mock data + current_price = Decimal("150.00") + price_change = Decimal("2.50") + price_change_percent = (price_change / (current_price - price_change)) * 100 + + # Generate mock historical data + historical_data = [] + base_time = datetime.now() - timedelta(days=1) + + for i in range(24): # 24 hours of hourly data + timestamp = base_time + timedelta(hours=i) + price_variation = Decimal(str(147 + (i * 0.5))) + + historical_data.append(PricePoint( + timestamp=timestamp, + open=price_variation, + high=price_variation + Decimal("1.0"), + low=price_variation - Decimal("1.0"), + close=price_variation + Decimal("0.5"), + volume=1000000 + (i * 10000) + )) + + return AssetPriceResponse( + symbol=symbol, + current_price=current_price, + price_change=price_change, + price_change_percent=price_change_percent, + period=period, + historical_data=historical_data, + last_updated=datetime.now() + ) + + except Exception as e: + logger.error(f"Failed to get price data for {symbol}", exc_info=True) + return None + + async def update_asset_cache(self, symbol: str) -> bool: + """Update cached market data for an asset.""" + logger.info(f"Updating asset cache: {symbol}") + + try: + # TODO: Implement cache update logic + # This would fetch latest data and update cache/database + return True + + except Exception as e: + logger.error(f"Failed to update cache for {symbol}", exc_info=True) + return False + + async def get_trending_assets(self, limit: int = 10) -> List[Asset]: + """Get trending assets based on volume or price movement.""" + logger.info(f"Getting trending assets: limit={limit}") + + # TODO: Implement trending logic based on actual market data + # For now, return most recently updated assets + return await self.repository.list_assets( + skip=0, + limit=limit, + filters={"is_active": True}, + order_by="updated_at", + order_desc=True + ) \ No newline at end of file diff --git a/python/valuecell/server/services/auth/__init__.py b/python/valuecell/server/services/auth/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/python/valuecell/server/services/i18n/__init__.py b/python/valuecell/server/services/i18n/__init__.py new file mode 100644 index 000000000..1310d0040 --- /dev/null +++ b/python/valuecell/server/services/i18n/__init__.py @@ -0,0 +1,5 @@ +"""I18n services for ValueCell Server.""" + +from .i18n_service import I18nService + +__all__ = ["I18nService"] \ No newline at end of file diff --git a/python/valuecell/server/services/i18n/i18n_service.py b/python/valuecell/server/services/i18n/i18n_service.py new file mode 100644 index 000000000..d29633a5a --- /dev/null +++ b/python/valuecell/server/services/i18n/i18n_service.py @@ -0,0 +1,311 @@ +"""I18n service for ValueCell Server.""" + +import json +from typing import Dict, Any, Optional, List +from datetime import datetime +from sqlalchemy.orm import Session +from pathlib import Path + +from ...config.logging import get_logger +from ...config.settings import get_settings + +logger = get_logger(__name__) + + +class I18nService: + """Service for handling internationalization.""" + + def __init__(self, db: Session): + """Initialize i18n service.""" + self.db = db + self.settings = get_settings() + self._translations = {} + self._load_translations() + + def _load_translations(self) -> None: + """Load translation files.""" + try: + # Define supported languages + self.supported_languages = { + "en-US": {"name": "English (US)", "native_name": "English"}, + "zh-Hans": {"name": "Chinese (Simplified)", "native_name": "简体中文"}, + "zh-Hant": {"name": "Chinese (Traditional)", "native_name": "繁體中文"}, + "ja-JP": {"name": "Japanese", "native_name": "日本語"}, + "ko-KR": {"name": "Korean", "native_name": "한국어"}, + } + + # Define supported timezones + self.supported_timezones = [ + "UTC", + "America/New_York", + "America/Los_Angeles", + "Europe/London", + "Europe/Paris", + "Asia/Shanghai", + "Asia/Tokyo", + "Asia/Seoul", + "Australia/Sydney", + ] + + # Load basic translations (in production, these would come from files) + self._translations = { + "en-US": { + "welcome": "Welcome to ValueCell", + "error.not_found": "Resource not found", + "error.internal": "Internal server error", + "success.created": "Resource created successfully", + "success.updated": "Resource updated successfully", + "success.deleted": "Resource deleted successfully", + }, + "zh-Hans": { + "welcome": "欢迎使用ValueCell", + "error.not_found": "资源未找到", + "error.internal": "内部服务器错误", + "success.created": "资源创建成功", + "success.updated": "资源更新成功", + "success.deleted": "资源删除成功", + }, + "zh-Hant": { + "welcome": "歡迎使用ValueCell", + "error.not_found": "資源未找到", + "error.internal": "內部伺服器錯誤", + "success.created": "資源創建成功", + "success.updated": "資源更新成功", + "success.deleted": "資源刪除成功", + }, + } + + logger.info("Translations loaded successfully") + + except Exception as e: + logger.error(f"Error loading translations: {e}") + # Fallback to minimal English translations + self._translations = { + "en-US": { + "welcome": "Welcome to ValueCell", + "error.not_found": "Resource not found", + "error.internal": "Internal server error", + } + } + + async def get_supported_languages(self) -> Dict[str, Any]: + """Get list of supported languages.""" + return { + "languages": self.supported_languages, + "default": self.settings.DEFAULT_LANGUAGE + } + + async def get_supported_timezones(self) -> Dict[str, Any]: + """Get list of supported timezones.""" + return { + "timezones": self.supported_timezones, + "default": self.settings.DEFAULT_TIMEZONE + } + + async def get_user_config(self, user_id: Optional[str]) -> Dict[str, Any]: + """Get i18n configuration for user.""" + # In a real implementation, this would fetch from database + # For now, return default configuration + return { + "language": self.settings.DEFAULT_LANGUAGE, + "timezone": self.settings.DEFAULT_TIMEZONE, + "date_format": "YYYY-MM-DD", + "time_format": "HH:mm:ss", + "currency": "USD", + "number_format": { + "decimal_separator": ".", + "thousands_separator": ",", + "decimal_places": 2 + } + } + + async def set_user_language(self, user_id: Optional[str], language: str) -> Dict[str, Any]: + """Set user language preference.""" + if language not in self.supported_languages: + raise ValueError(f"Unsupported language: {language}") + + # In a real implementation, this would save to database + logger.info(f"Setting language to {language} for user {user_id}") + + return { + "language": language, + "message": f"Language set to {language}" + } + + async def set_user_timezone(self, user_id: Optional[str], timezone: str) -> Dict[str, Any]: + """Set user timezone preference.""" + if timezone not in self.supported_timezones: + raise ValueError(f"Unsupported timezone: {timezone}") + + # In a real implementation, this would save to database + logger.info(f"Setting timezone to {timezone} for user {user_id}") + + return { + "timezone": timezone, + "message": f"Timezone set to {timezone}" + } + + async def detect_language(self, accept_language: str) -> Dict[str, Any]: + """Detect language from Accept-Language header.""" + # Simple language detection logic + # In production, use a proper language detection library + + languages = accept_language.lower().split(",") + detected_language = self.settings.DEFAULT_LANGUAGE + + for lang in languages: + lang = lang.strip().split(";")[0] # Remove quality values + if lang in self.supported_languages: + detected_language = lang + break + # Check for language family matches + elif lang.startswith("zh"): + detected_language = "zh-Hans" + break + elif lang.startswith("en"): + detected_language = "en-US" + break + + return { + "detected_language": detected_language, + "confidence": 0.8, # Mock confidence score + "supported": detected_language in self.supported_languages + } + + async def translate(self, key: str, language: str, variables: Optional[Dict[str, Any]] = None) -> str: + """Translate a key to target language.""" + if language not in self.supported_languages: + language = self.settings.DEFAULT_LANGUAGE + + translations = self._translations.get(language, self._translations.get(self.settings.DEFAULT_LANGUAGE, {})) + translation = translations.get(key, key) + + # Simple variable substitution + if variables: + for var_key, var_value in variables.items(): + translation = translation.replace(f"{{{var_key}}}", str(var_value)) + + return translation + + async def format_datetime(self, datetime_str: str, format_type: str, user_id: Optional[str] = None) -> str: + """Format datetime according to user preferences.""" + try: + # Parse datetime string + dt = datetime.fromisoformat(datetime_str.replace("Z", "+00:00")) + + # Get user config (in real implementation, from database) + config = await self.get_user_config(user_id) + + # Format based on type + if format_type == "date": + return dt.strftime("%Y-%m-%d") + elif format_type == "time": + return dt.strftime("%H:%M:%S") + elif format_type == "datetime": + return dt.strftime("%Y-%m-%d %H:%M:%S") + else: + return dt.isoformat() + + except Exception as e: + logger.error(f"Error formatting datetime: {e}") + return datetime_str + + async def format_number(self, number: float, decimal_places: Optional[int] = None, user_id: Optional[str] = None) -> str: + """Format number according to user preferences.""" + try: + config = await self.get_user_config(user_id) + number_format = config.get("number_format", {}) + + if decimal_places is None: + decimal_places = number_format.get("decimal_places", 2) + + # Format number + formatted = f"{number:,.{decimal_places}f}" + + # Apply user preferences + decimal_sep = number_format.get("decimal_separator", ".") + thousands_sep = number_format.get("thousands_separator", ",") + + if decimal_sep != ".": + formatted = formatted.replace(".", "__DECIMAL__") + if thousands_sep != ",": + formatted = formatted.replace(",", thousands_sep) + if decimal_sep != ".": + formatted = formatted.replace("__DECIMAL__", decimal_sep) + + return formatted + + except Exception as e: + logger.error(f"Error formatting number: {e}") + return str(number) + + async def format_currency(self, amount: float, decimal_places: Optional[int] = None, user_id: Optional[str] = None) -> str: + """Format currency according to user preferences.""" + try: + config = await self.get_user_config(user_id) + currency = config.get("currency", "USD") + + # Format as number first + formatted_number = await self.format_number(amount, decimal_places, user_id) + + # Add currency symbol + currency_symbols = { + "USD": "$", + "EUR": "€", + "GBP": "£", + "JPY": "¥", + "CNY": "¥", + "KRW": "₩" + } + + symbol = currency_symbols.get(currency, currency) + return f"{symbol}{formatted_number}" + + except Exception as e: + logger.error(f"Error formatting currency: {e}") + return str(amount) + + async def get_user_settings(self, user_id: str) -> Dict[str, Any]: + """Get user i18n settings.""" + # In real implementation, fetch from database + return await self.get_user_config(user_id) + + async def update_user_settings(self, user_id: str, language: Optional[str] = None, timezone: Optional[str] = None) -> Dict[str, Any]: + """Update user i18n settings.""" + settings = {} + + if language: + if language not in self.supported_languages: + raise ValueError(f"Unsupported language: {language}") + settings["language"] = language + + if timezone: + if timezone not in self.supported_timezones: + raise ValueError(f"Unsupported timezone: {timezone}") + settings["timezone"] = timezone + + # In real implementation, save to database + logger.info(f"Updated settings for user {user_id}: {settings}") + + # Return updated config + config = await self.get_user_config(user_id) + config.update(settings) + return config + + async def get_agent_context(self, user_id: Optional[str], session_id: Optional[str]) -> Dict[str, Any]: + """Get i18n context for agent execution.""" + config = await self.get_user_config(user_id) + + return { + "user_id": user_id, + "session_id": session_id, + "language": config["language"], + "timezone": config["timezone"], + "locale_info": { + "date_format": config["date_format"], + "time_format": config["time_format"], + "currency": config["currency"], + "number_format": config["number_format"] + }, + "translations": self._translations.get(config["language"], {}) + } \ No newline at end of file From 2537659b9b31d9fb98dfe1ef37f565bb34218bb0 Mon Sep 17 00:00:00 2001 From: zhonghao lu Date: Mon, 15 Sep 2025 15:17:11 +0800 Subject: [PATCH 02/10] remve useless code --- python/valuecell/api/i18n_api.py | 433 ------------ .../examples/asset_adapter_example.py | 361 ---------- python/valuecell/examples/i18n_example.py | 165 ----- python/valuecell/i18n.py | 8 +- python/valuecell/server/api/app.py | 5 +- .../valuecell/server/api/routers/__init__.py | 5 - python/valuecell/server/api/routers/agents.py | 93 --- python/valuecell/server/api/routers/assets.py | 71 -- python/valuecell/server/api/routers/health.py | 41 -- python/valuecell/server/api/routers/i18n.py | 288 -------- .../server/services/agents/__init__.py | 0 .../server/services/agents/agent_service.py | 137 ---- .../server/services/assets/__init__.py | 0 .../server/services/assets/asset_service.py | 160 ----- .../server/services/auth/__init__.py | 0 .../server/services/i18n/__init__.py | 5 - .../server/services/i18n/i18n_service.py | 311 --------- python/valuecell/services/__init__.py | 11 +- python/valuecell/services/agent_context.py | 38 +- .../services/application/__init__.py | 0 python/valuecell/services/assets/__init__.py | 51 -- .../services/assets/asset_service.py | 636 ------------------ python/valuecell/services/i18n_service.py | 325 --------- python/valuecell/utils/i18n_utils.py | 426 ------------ 24 files changed, 18 insertions(+), 3552 deletions(-) delete mode 100644 python/valuecell/api/i18n_api.py delete mode 100644 python/valuecell/examples/asset_adapter_example.py delete mode 100644 python/valuecell/examples/i18n_example.py delete mode 100644 python/valuecell/server/api/routers/agents.py delete mode 100644 python/valuecell/server/api/routers/assets.py delete mode 100644 python/valuecell/server/api/routers/health.py delete mode 100644 python/valuecell/server/api/routers/i18n.py delete mode 100644 python/valuecell/server/services/agents/__init__.py delete mode 100644 python/valuecell/server/services/agents/agent_service.py delete mode 100644 python/valuecell/server/services/assets/__init__.py delete mode 100644 python/valuecell/server/services/assets/asset_service.py delete mode 100644 python/valuecell/server/services/auth/__init__.py delete mode 100644 python/valuecell/server/services/i18n/__init__.py delete mode 100644 python/valuecell/server/services/i18n/i18n_service.py delete mode 100644 python/valuecell/services/application/__init__.py delete mode 100644 python/valuecell/services/assets/__init__.py delete mode 100644 python/valuecell/services/assets/asset_service.py delete mode 100644 python/valuecell/services/i18n_service.py delete mode 100644 python/valuecell/utils/i18n_utils.py diff --git a/python/valuecell/api/i18n_api.py b/python/valuecell/api/i18n_api.py deleted file mode 100644 index 8212af614..000000000 --- a/python/valuecell/api/i18n_api.py +++ /dev/null @@ -1,433 +0,0 @@ -"""Standalone i18n API module for ValueCell.""" - -from typing import Dict, Any, Optional -from fastapi import APIRouter, HTTPException, Header -from datetime import datetime - -from .schemas import ( - SuccessResponse, - LanguageRequest, - TimezoneRequest, - LanguageDetectionRequest, - TranslationRequest, - DateTimeFormatRequest, - NumberFormatRequest, - CurrencyFormatRequest, - UserI18nSettingsRequest, - AgentI18nContext, -) -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 ( - detect_browser_language, - get_common_timezones, - get_timezone_display_name, - validate_language_code, - validate_timezone, -) - - -class I18nAPI: - """Standalone i18n API class.""" - - def __init__(self): - """Initialize i18n API.""" - self.i18n_service = get_i18n_service() - self.settings = get_settings() - - # User context storage (in production, use Redis or database) - self._user_contexts: Dict[str, Dict[str, Any]] = {} - - # Create router - self.router = self._create_router() - - def _create_router(self) -> APIRouter: - """Create FastAPI router for i18n endpoints.""" - 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"] - ) - 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"]) - - # 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"]) - - # 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"] - ) - - # Agent context - router.add_api_route("/agent/context", self.get_agent_context, methods=["GET"]) - - return router - - def _get_user_context(self, user_id: Optional[str]) -> Dict[str, Any]: - """Get user context and apply to i18n service.""" - if user_id and user_id in self._user_contexts: - user_context = self._user_contexts[user_id] - self.i18n_service.set_language(user_context.get("language", "en-US")) - self.i18n_service.set_timezone(user_context.get("timezone", "UTC")) - return user_context - return {"language": "en-US", "timezone": "UTC"} - - 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: - """Get current i18n configuration.""" - self._get_user_context(user_id) - - return SuccessResponse( - message="I18n configuration retrieved successfully", - data=self.i18n_service.to_dict(), - ) - - async def get_supported_languages(self) -> SuccessResponse: - """Get supported languages.""" - languages = [ - { - "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(), - }, - ) - - async def set_language( - self, - request: LanguageRequest, - user_id: Optional[str] = Header(None, alias="X-User-ID"), - ) -> SuccessResponse: - """Set current language.""" - if not validate_language_code(request.language): - raise HTTPException( - status_code=400, - detail=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") - - # Save user context - if user_id: - if user_id not in self._user_contexts: - self._user_contexts[user_id] = {} - self._user_contexts[user_id]["language"] = request.language - self._user_contexts[user_id]["timezone"] = ( - self.i18n_service.get_current_timezone() - ) - - return SuccessResponse( - message="Language updated successfully", - data={ - "language": request.language, - "timezone": self.i18n_service.get_current_timezone(), - }, - ) - - async def get_timezones(self) -> SuccessResponse: - """Get available timezones.""" - common_timezones = get_common_timezones() - timezone_list = [ - { - "value": tz, - "label": get_timezone_display_name(tz), - "is_current": tz == self.i18n_service.get_current_timezone(), - } - for tz in common_timezones - ] - - # Add language-specific timezones if not in common list - for lang_tz in LANGUAGE_TIMEZONE_MAPPING.values(): - if lang_tz not in common_timezones: - timezone_list.append( - { - "value": lang_tz, - "label": get_timezone_display_name(lang_tz), - "is_current": lang_tz - == self.i18n_service.get_current_timezone(), - } - ) - - # Sort by label - timezone_list.sort(key=lambda x: x["label"]) - - return SuccessResponse( - message="Timezones retrieved successfully", - data={ - "timezones": timezone_list, - "current": self.i18n_service.get_current_timezone(), - }, - ) - - async def set_timezone( - self, - request: TimezoneRequest, - user_id: Optional[str] = Header(None, alias="X-User-ID"), - ) -> SuccessResponse: - """Set current timezone.""" - if not validate_timezone(request.timezone): - raise HTTPException( - status_code=400, detail=f"Timezone '{request.timezone}' is not valid" - ) - - success = self.i18n_service.set_timezone(request.timezone) - if not success: - raise HTTPException(status_code=500, detail="Failed to set timezone") - - # Save user context - if user_id: - if user_id not in self._user_contexts: - self._user_contexts[user_id] = {} - self._user_contexts[user_id]["timezone"] = request.timezone - - return SuccessResponse( - message="Timezone updated successfully", - data={ - "timezone": request.timezone, - "display_name": get_timezone_display_name(request.timezone), - }, - ) - - async def detect_language( - self, request: LanguageDetectionRequest - ) -> SuccessResponse: - """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], - }, - ) - - async def translate(self, request: TranslationRequest) -> SuccessResponse: - """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, - }, - ) - except Exception as e: - raise HTTPException( - status_code=500, - detail=f"Failed to translate key '{request.key}': {str(e)}", - ) - - async def format_datetime(self, request: DateTimeFormatRequest) -> SuccessResponse: - """Format datetime.""" - try: - # Parse ISO datetime string - dt = datetime.fromisoformat(request.datetime.replace("Z", "+00:00")) - formatted_dt = self.i18n_service.format_datetime(dt, request.format_type) - - return SuccessResponse( - message="Datetime formatted successfully", - data={ - "original": request.datetime, - "formatted": formatted_dt, - "format_type": request.format_type, - "language": self.i18n_service.get_current_language(), - "timezone": self.i18n_service.get_current_timezone(), - }, - ) - except Exception as e: - raise HTTPException( - status_code=400, detail=f"Failed to format datetime: {str(e)}" - ) - - async def format_number(self, request: NumberFormatRequest) -> SuccessResponse: - """Format number.""" - try: - formatted_number = self.i18n_service.format_number( - request.number, request.decimal_places - ) - - return SuccessResponse( - message="Number formatted successfully", - data={ - "original": request.number, - "formatted": formatted_number, - "decimal_places": request.decimal_places, - "language": self.i18n_service.get_current_language(), - }, - ) - except Exception as e: - raise HTTPException( - status_code=500, detail=f"Failed to format number: {str(e)}" - ) - - async def format_currency(self, request: CurrencyFormatRequest) -> SuccessResponse: - """Format currency.""" - try: - formatted_currency = self.i18n_service.format_currency( - request.amount, request.decimal_places - ) - - return SuccessResponse( - message="Currency formatted successfully", - data={ - "original": request.amount, - "formatted": formatted_currency, - "decimal_places": request.decimal_places, - "language": self.i18n_service.get_current_language(), - "currency_symbol": self.i18n_service._i18n_config.get_currency_symbol(), - }, - ) - except Exception as e: - raise HTTPException( - status_code=500, detail=f"Failed to format currency: {str(e)}" - ) - - async def get_user_settings( - self, user_id: str = Header(..., alias="X-User-ID") - ) -> SuccessResponse: - """Get user's i18n settings.""" - user_context = self._user_contexts.get( - user_id, {"language": "en-US", "timezone": "UTC"} - ) - - return SuccessResponse( - message="User i18n settings retrieved successfully", - data={ - "user_id": user_id, - "language": user_context.get("language", "en-US"), - "timezone": user_context.get("timezone", "UTC"), - }, - ) - - async def update_user_settings( - self, - request: UserI18nSettingsRequest, - user_id: str = Header(..., alias="X-User-ID"), - ) -> SuccessResponse: - """Update user's i18n settings.""" - if user_id not in self._user_contexts: - self._user_contexts[user_id] = {} - - user_context = self._user_contexts[user_id] - - if request.language: - if not validate_language_code(request.language): - raise HTTPException( - status_code=400, - detail=f"Language '{request.language}' is not supported", - ) - user_context["language"] = request.language - self.i18n_service.set_language(request.language) - - if request.timezone: - if not validate_timezone(request.timezone): - raise HTTPException( - status_code=400, - detail=f"Timezone '{request.timezone}' is not valid", - ) - user_context["timezone"] = request.timezone - self.i18n_service.set_timezone(request.timezone) - - return SuccessResponse( - message="User i18n settings updated successfully", - data={ - "user_id": user_id, - "language": user_context.get("language"), - "timezone": user_context.get("timezone"), - }, - ) - - 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: - """Get i18n context for agent communication.""" - # Load user-specific settings - self._get_user_context(user_id) - - context = AgentI18nContext( - language=self.i18n_service.get_current_language(), - timezone=self.i18n_service.get_current_timezone(), - currency_symbol=self.i18n_service._i18n_config.get_currency_symbol(), - date_format=self.i18n_service._i18n_config.get_date_format(), - time_format=self.i18n_service._i18n_config.get_time_format(), - number_format=self.i18n_service._i18n_config.get_number_format(), - user_id=user_id, - session_id=session_id, - ) - - return SuccessResponse( - message="Agent i18n context retrieved successfully", data=context.dict() - ) - - def get_user_context(self, user_id: str) -> Dict[str, Any]: - """Get user context for agents.""" - return self._user_contexts.get( - user_id, {"language": "en-US", "timezone": "UTC"} - ) - - def set_user_context(self, user_id: str, context: Dict[str, Any]): - """Set user context for agents.""" - if user_id not in self._user_contexts: - self._user_contexts[user_id] = {} - self._user_contexts[user_id].update(context) - - -# Global i18n API instance -_i18n_api: Optional[I18nAPI] = None - - -def get_i18n_api() -> I18nAPI: - """Get global i18n API instance.""" - global _i18n_api - if _i18n_api is None: - _i18n_api = I18nAPI() - return _i18n_api - - -def create_i18n_router() -> APIRouter: - """Create i18n router for inclusion in main app.""" - return get_i18n_api().router diff --git a/python/valuecell/examples/asset_adapter_example.py b/python/valuecell/examples/asset_adapter_example.py deleted file mode 100644 index 3b82c0c5d..000000000 --- a/python/valuecell/examples/asset_adapter_example.py +++ /dev/null @@ -1,361 +0,0 @@ -"""Example usage of the ValueCell Asset Data Adapter system. - -This example demonstrates how to configure and use the asset data adapters -for financial data retrieval, search, and watchlist management with i18n support. -""" - -import logging - -from valuecell.adapters.assets import get_adapter_manager -from valuecell.services.assets import ( - get_asset_service, - search_assets, - get_asset_info, - get_asset_price, - add_to_watchlist, - get_watchlist, -) -from valuecell.i18n import set_i18n_config, I18nConfig - -# Configure logging -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) - - -def setup_adapters(): - """Configure and initialize data adapters.""" - logger.info("Setting up data adapters...") - - # Get adapter manager - manager = get_adapter_manager() - - # Configure Yahoo Finance (free, no API key required) - try: - manager.configure_yfinance() - logger.info("✓ Yahoo Finance adapter configured") - except Exception as e: - logger.warning(f"✗ Yahoo Finance adapter failed: {e}") - - # Configure TuShare (requires API key) - try: - # Replace with your actual TuShare API key - tushare_api_key = "your_tushare_api_key_here" - if tushare_api_key != "your_tushare_api_key_here": - manager.configure_tushare(api_key=tushare_api_key) - logger.info("✓ TuShare adapter configured") - else: - logger.warning("✗ TuShare API key not provided, skipping") - except Exception as e: - logger.warning(f"✗ TuShare adapter failed: {e}") - - # Configure CoinMarketCap (requires API key for crypto data) - try: - # Replace with your actual CoinMarketCap API key - cmc_api_key = "your_coinmarketcap_api_key_here" - if cmc_api_key != "your_coinmarketcap_api_key_here": - manager.configure_coinmarketcap(api_key=cmc_api_key) - logger.info("✓ CoinMarketCap adapter configured") - else: - logger.warning("✗ CoinMarketCap API key not provided, skipping") - except Exception as e: - logger.warning(f"✗ CoinMarketCap adapter failed: {e}") - - # Configure AKShare (free, no API key required) - # Now supports A-shares, US stocks, Hong Kong stocks, and cryptocurrencies - try: - manager.configure_akshare() - logger.info( - "✓ AKShare adapter configured (supports A-shares, HK stocks, US stocks, and crypto)" - ) - except Exception as e: - logger.warning(f"✗ AKShare adapter failed: {e}") - - # Configure Finnhub (requires API key) - try: - # Replace with your actual Finnhub API key - finnhub_api_key = "your_finnhub_api_key_here" - if finnhub_api_key != "your_finnhub_api_key_here": - manager.configure_finnhub(api_key=finnhub_api_key) - logger.info("✓ Finnhub adapter configured") - else: - logger.warning("✗ Finnhub API key not provided, skipping") - except Exception as e: - logger.warning(f"✗ Finnhub adapter failed: {e}") - - # Check system health - service = get_asset_service() - health = service.get_system_health() - logger.info( - f"System health: {health['overall_status']} " - f"({health['healthy_adapters']}/{health['total_adapters']} adapters)" - ) - - return manager - - -def demonstrate_asset_search(): - """Demonstrate asset search functionality with i18n.""" - logger.info("\n=== Asset Search Demo ===") - - # Search in English - logger.info("Searching for 'AAPL' in English...") - results_en = search_assets("AAPL", language="en-US", limit=5) - - if results_en["success"]: - logger.info(f"Found {results_en['count']} results:") - for result in results_en["results"]: - logger.info( - f" - {result['ticker']}: {result['display_name']} " - f"({result['asset_type_display']})" - ) - - # Search in Chinese - logger.info("\nSearching for '00700.HK' in Chinese...") - results_zh = search_assets("00700.HK", language="zh-Hans", limit=5) - - if results_zh["success"]: - logger.info(f"找到 {results_zh['count']} 个结果:") - for result in results_zh["results"]: - logger.info( - f" - {result['ticker']}: {result['display_name']} " - f"({result['asset_type_display']})" - ) - - # Search for Chinese stocks - logger.info("\nSearching for Chinese stocks...") - results_cn = search_assets("600519", asset_types=["stock"], limit=3) - - if results_cn["success"]: - logger.info(f"Found {results_cn['count']} Chinese stocks:") - for result in results_cn["results"]: - logger.info(f" - {result['ticker']}: {result['display_name']}") - - # Search for cryptocurrencies - logger.info("\nSearching for cryptocurrencies...") - results_crypto = search_assets("BTC-USD", asset_types=["crypto"], limit=3) - - if results_crypto["success"]: - logger.info(f"Found {results_crypto['count']} cryptocurrencies:") - for result in results_crypto["results"]: - logger.info(f" - {result['ticker']}: {result['display_name']}") - - # Demonstrate enhanced AKShare multi-market search - logger.info("\n=== Enhanced Multi-Market Search (AKShare) ===") - - # Search Hong Kong stocks - logger.info("Searching for Hong Kong stocks (Tencent)...") - hk_results = search_assets("00700", limit=3) - if hk_results["success"]: - for result in hk_results["results"]: - logger.info(f" - HK Stock: {result['ticker']}: {result['display_name']}") - - # Search US stocks through AKShare - logger.info("\nSearching for US stocks through AKShare...") - us_results = search_assets("AAPL", exchanges=["NASDAQ", "NYSE"], limit=3) - if us_results["success"]: - for result in us_results["results"]: - logger.info(f" - US Stock: {result['ticker']}: {result['display_name']}") - - # Direct ticker lookup fallback - logger.info("\nDemonstrating semantic search fallback...") - direct_results = search_assets( - "000001", limit=3 - ) # Should find through direct lookup - if direct_results["success"]: - for result in direct_results["results"]: - logger.info( - f" - Direct Match: {result['ticker']}: {result['display_name']}" - ) - - -def demonstrate_asset_info(): - """Demonstrate getting detailed asset information.""" - logger.info("\n=== Asset Information Demo ===") - - # Get info for Apple stock - tickers = ["NASDAQ:AAPL", "HKEX:700", "SSE:600519", "CRYPTO:BTC"] - - for ticker in tickers: - logger.info(f"\nGetting info for {ticker}...") - - # Get in English - info_en = get_asset_info(ticker, language="en-US") - if info_en["success"]: - logger.info( - f" English: {info_en['display_name']} " - f"({info_en['asset_type_display']})" - ) - logger.info(f" Exchange: {info_en['market_info']['exchange']}") - logger.info(f" Country: {info_en['market_info']['country']}") - - # Get in Chinese - info_zh = get_asset_info(ticker, language="zh-Hans") - if info_zh["success"]: - logger.info( - f" 中文: {info_zh['display_name']} ({info_zh['asset_type_display']})" - ) - - -def demonstrate_price_data(): - """Demonstrate real-time price data retrieval.""" - logger.info("\n=== Price Data Demo ===") - - tickers = ["NASDAQ:AAPL", "HKEX:700", "SSE:600519", "CRYPTO:BTC"] - - # Get individual price - logger.info("Getting individual price for AAPL...") - price_data = get_asset_price("NASDAQ:AAPL", language="en-US") - - if price_data["success"]: - logger.info(f" Price: {price_data['price_formatted']}") - if price_data["change_percent_formatted"]: - logger.info(f" Change: {price_data['change_percent_formatted']}") - if price_data["market_cap_formatted"]: - logger.info(f" Market Cap: {price_data['market_cap_formatted']}") - - # Get multiple prices - logger.info(f"\nGetting prices for multiple assets: {tickers}") - service = get_asset_service() - prices_data = service.get_multiple_prices(tickers, language="en-US") - - if prices_data["success"]: - logger.info(f"Successfully retrieved {prices_data['count']} prices:") - for ticker, price_info in prices_data["prices"].items(): - if price_info: - logger.info( - f" {ticker}: {price_info['price_formatted']} " - f"({price_info.get('change_percent_formatted', 'N/A')})" - ) - else: - logger.info(f" {ticker}: Price not available") - - -def demonstrate_watchlist_management(): - """Demonstrate watchlist creation and management.""" - logger.info("\n=== Watchlist Management Demo ===") - - user_id = "demo_user_123" - service = get_asset_service() - - # Create a watchlist - logger.info("Creating a new watchlist...") - create_result = service.create_watchlist( - user_id=user_id, - name="My Tech Stocks", - description="Technology companies I'm watching", - is_default=True, - ) - - if create_result["success"]: - logger.info("✓ Watchlist created successfully") - - # Add assets to watchlist - assets_to_add = [ - ("NASDAQ:AAPL", "Apple - iPhone maker"), - ("HKEX:700", "Tencent - Chinese tech giant"), - ("SSE:600519", "Kweichow Moutai - Chinese liquor company"), - ("CRYPTO:BTC", "Bitcoin - First and largest cryptocurrency"), - ] - - logger.info("Adding assets to watchlist...") - for ticker, notes in assets_to_add: - result = add_to_watchlist(user_id=user_id, ticker=ticker, notes=notes) - if result["success"]: - logger.info(f" ✓ Added {ticker}") - else: - logger.warning(f" ✗ Failed to add {ticker}: {result.get('error')}") - - # Get watchlist with prices - logger.info("\nRetrieving watchlist with current prices...") - watchlist_data = get_watchlist( - user_id=user_id, include_prices=True, language="zh-Hans" - ) - - if watchlist_data["success"]: - watchlist = watchlist_data["watchlist"] - logger.info(f"Watchlist: {watchlist['name']}") - logger.info(f"Number of assets: {watchlist['items_count']}") - - for asset in watchlist["assets"]: - display_name = asset["display_name"] - notes = asset["notes"] - - price_info = "" - if "price_data" in asset and asset["price_data"]: - price_data = asset["price_data"] - price_info = f" - {price_data['price_formatted']}" - if price_data.get("change_percent_formatted"): - price_info += f" ({price_data['change_percent_formatted']})" - - logger.info(f" • {display_name}{price_info}") - if notes: - logger.info(f" Notes: {notes}") - - # List all user watchlists - logger.info("\nListing all user watchlists...") - all_watchlists = service.get_user_watchlists(user_id) - - if all_watchlists["success"]: - logger.info(f"User {user_id} has {all_watchlists['count']} watchlists:") - for wl in all_watchlists["watchlists"]: - default_marker = " (Default)" if wl["is_default"] else "" - logger.info( - f" • {wl['name']}{default_marker} - {wl['items_count']} assets" - ) - - -def demonstrate_i18n_features(): - """Demonstrate internationalization features.""" - logger.info("\n=== Internationalization Demo ===") - - # Test different languages - languages = ["en-US", "zh-Hans", "zh-Hant"] - ticker = "NASDAQ:AAPL" - - for lang in languages: - logger.info(f"\nTesting language: {lang}") - - # Set language configuration - config = I18nConfig(language=lang) - set_i18n_config(config) - - # Search for assets - results = search_assets("APPL", language=lang, limit=1) - if results["success"] and results["results"]: - result = results["results"][0] - logger.info( - f" Search result: {result['display_name']} ({result['asset_type_display']})" - ) - - # Get price with localized formatting - price_data = get_asset_price(ticker, language=lang) - if price_data["success"]: - logger.info(f" Price: {price_data['price_formatted']}") - if price_data.get("change_percent_formatted"): - logger.info(f" Change: {price_data['change_percent_formatted']}") - - -def main(): - """Main demonstration function.""" - logger.info("=== ValueCell Asset Data Adapter Demo ===") - - try: - # Setup adapters - setup_adapters() - - # Run demonstrations - demonstrate_asset_search() - demonstrate_asset_info() - demonstrate_price_data() - demonstrate_watchlist_management() - demonstrate_i18n_features() - - logger.info("\n=== Demo completed successfully! ===") - - except Exception as e: - logger.error(f"Demo failed with error: {e}") - raise - - -if __name__ == "__main__": - main() 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 index d09924045..04a173773 100644 --- a/python/valuecell/i18n.py +++ b/python/valuecell/i18n.py @@ -4,13 +4,7 @@ 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, -) +# Core i18n functionality removed # Configuration from .config.settings import get_settings diff --git a/python/valuecell/server/api/app.py b/python/valuecell/server/api/app.py index f3a92b2c5..34561f4dc 100644 --- a/python/valuecell/server/api/app.py +++ b/python/valuecell/server/api/app.py @@ -5,7 +5,6 @@ from contextlib import asynccontextmanager from ..config.settings import get_settings -from .routers import health, agents, assets, i18n def create_app() -> FastAPI: @@ -55,6 +54,4 @@ def _add_middleware(app: FastAPI, settings) -> None: def _add_routes(app: FastAPI) -> None: """Add routes to the application.""" app.include_router(health.router, prefix="/health", tags=["health"]) - app.include_router(agents.router, prefix="/api/v1/agents", tags=["agents"]) - app.include_router(assets.router, prefix="/api/v1/assets", tags=["assets"]) - app.include_router(i18n.router, prefix="/api/v1", tags=["i18n"]) \ No newline at end of file + app.include_router(agents.router, prefix="/api/v1", tags=["agents"]) \ No newline at end of file diff --git a/python/valuecell/server/api/routers/__init__.py b/python/valuecell/server/api/routers/__init__.py index 7d41cfdcc..e69de29bb 100644 --- a/python/valuecell/server/api/routers/__init__.py +++ b/python/valuecell/server/api/routers/__init__.py @@ -1,5 +0,0 @@ -"""API routers for ValueCell Server.""" - -from . import health, agents, assets, i18n - -__all__ = ["health", "agents", "assets", "i18n"] \ No newline at end of file diff --git a/python/valuecell/server/api/routers/agents.py b/python/valuecell/server/api/routers/agents.py deleted file mode 100644 index f28cc748d..000000000 --- a/python/valuecell/server/api/routers/agents.py +++ /dev/null @@ -1,93 +0,0 @@ -"""Agents router for ValueCell Server.""" - -from typing import List -from fastapi import APIRouter, Depends, HTTPException, status -from sqlalchemy.orm import Session - -from ...config.database import get_db -from ...services.agents.agent_service import AgentService -from ..schemas.agents import ( - AgentResponse, - AgentCreateRequest, - AgentUpdateRequest, - AgentExecutionRequest, - AgentExecutionResponse, -) - -router = APIRouter() - - -@router.get("/", response_model=List[AgentResponse]) -async def list_agents(db: Session = Depends(get_db)): - """List all available agents.""" - agent_service = AgentService(db) - return await agent_service.list_agents() - - -@router.get("/{agent_id}", response_model=AgentResponse) -async def get_agent(agent_id: str, db: Session = Depends(get_db)): - """Get agent by ID.""" - agent_service = AgentService(db) - agent = await agent_service.get_agent(agent_id) - if not agent: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Agent not found" - ) - return agent - - -@router.post("/", response_model=AgentResponse, status_code=status.HTTP_201_CREATED) -async def create_agent( - agent_data: AgentCreateRequest, - db: Session = Depends(get_db) -): - """Create a new agent.""" - agent_service = AgentService(db) - return await agent_service.create_agent(agent_data) - - -@router.put("/{agent_id}", response_model=AgentResponse) -async def update_agent( - agent_id: str, - agent_data: AgentUpdateRequest, - db: Session = Depends(get_db) -): - """Update an existing agent.""" - agent_service = AgentService(db) - agent = await agent_service.update_agent(agent_id, agent_data) - if not agent: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Agent not found" - ) - return agent - - -@router.delete("/{agent_id}", status_code=status.HTTP_204_NO_CONTENT) -async def delete_agent(agent_id: str, db: Session = Depends(get_db)): - """Delete an agent.""" - agent_service = AgentService(db) - success = await agent_service.delete_agent(agent_id) - if not success: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Agent not found" - ) - - -@router.post("/{agent_id}/execute", response_model=AgentExecutionResponse) -async def execute_agent( - agent_id: str, - execution_request: AgentExecutionRequest, - db: Session = Depends(get_db) -): - """Execute an agent with given input.""" - agent_service = AgentService(db) - result = await agent_service.execute_agent(agent_id, execution_request) - if not result: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Agent not found" - ) - return result \ No newline at end of file diff --git a/python/valuecell/server/api/routers/assets.py b/python/valuecell/server/api/routers/assets.py deleted file mode 100644 index 916bfb478..000000000 --- a/python/valuecell/server/api/routers/assets.py +++ /dev/null @@ -1,71 +0,0 @@ -"""Assets router for ValueCell Server.""" - -from typing import List, Optional -from fastapi import APIRouter, Depends, HTTPException, Query, status -from sqlalchemy.orm import Session - -from ...config.database import get_db -from ...services.assets.asset_service import AssetService -from ..schemas.assets import ( - AssetResponse, - AssetPriceResponse, - AssetSearchRequest, -) - -router = APIRouter() - - -@router.get("/search", response_model=List[AssetResponse]) -async def search_assets( - query: str = Query(..., description="Search query for assets"), - limit: int = Query(10, ge=1, le=100, description="Maximum number of results"), - db: Session = Depends(get_db) -): - """Search for assets by symbol or name.""" - asset_service = AssetService(db) - return await asset_service.search_assets(query, limit) - - -@router.get("/{symbol}", response_model=AssetResponse) -async def get_asset( - symbol: str, - db: Session = Depends(get_db) -): - """Get asset information by symbol.""" - asset_service = AssetService(db) - asset = await asset_service.get_asset(symbol) - if not asset: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"Asset with symbol '{symbol}' not found" - ) - return asset - - -@router.get("/{symbol}/price", response_model=AssetPriceResponse) -async def get_asset_price( - symbol: str, - period: Optional[str] = Query("1d", description="Time period (1d, 1w, 1m, 3m, 6m, 1y)"), - db: Session = Depends(get_db) -): - """Get current and historical price data for an asset.""" - asset_service = AssetService(db) - price_data = await asset_service.get_asset_price(symbol, period) - if not price_data: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"Price data for asset '{symbol}' not found" - ) - return price_data - - -@router.get("/", response_model=List[AssetResponse]) -async def list_assets( - skip: int = Query(0, ge=0, description="Number of records to skip"), - limit: int = Query(50, ge=1, le=100, description="Maximum number of results"), - asset_type: Optional[str] = Query(None, description="Filter by asset type (stock, crypto, forex)"), - db: Session = Depends(get_db) -): - """List assets with pagination and filtering.""" - asset_service = AssetService(db) - return await asset_service.list_assets(skip, limit, asset_type) \ No newline at end of file diff --git a/python/valuecell/server/api/routers/health.py b/python/valuecell/server/api/routers/health.py deleted file mode 100644 index acae3bdb6..000000000 --- a/python/valuecell/server/api/routers/health.py +++ /dev/null @@ -1,41 +0,0 @@ -"""Health check router for ValueCell Server.""" - -from fastapi import APIRouter, Depends -from sqlalchemy.orm import Session -from ...config.database import get_db -from ...config.settings import get_settings -from ..schemas.health import HealthResponse - -router = APIRouter() - - -@router.get("/", response_model=HealthResponse) -async def health_check(db: Session = Depends(get_db)): - """Health check endpoint.""" - settings = get_settings() - - # Test database connection - try: - db.execute("SELECT 1") - db_status = "healthy" - except Exception as e: - db_status = f"unhealthy: {str(e)}" - - return HealthResponse( - status="healthy", - version=settings.APP_VERSION, - environment=settings.APP_ENVIRONMENT, - database=db_status, - ) - - -@router.get("/ready") -async def readiness_check(): - """Readiness check endpoint.""" - return {"status": "ready"} - - -@router.get("/live") -async def liveness_check(): - """Liveness check endpoint.""" - return {"status": "alive"} \ No newline at end of file diff --git a/python/valuecell/server/api/routers/i18n.py b/python/valuecell/server/api/routers/i18n.py deleted file mode 100644 index 137f7f33b..000000000 --- a/python/valuecell/server/api/routers/i18n.py +++ /dev/null @@ -1,288 +0,0 @@ -"""I18n router for ValueCell Server.""" - -from typing import Dict, Any, Optional -from fastapi import APIRouter, HTTPException, Header, Depends -from datetime import datetime -from sqlalchemy.orm import Session - -from ..schemas.common import SuccessResponse -from ..schemas.i18n import ( - LanguageRequest, - TimezoneRequest, - LanguageDetectionRequest, - TranslationRequest, - DateTimeFormatRequest, - NumberFormatRequest, - CurrencyFormatRequest, - UserI18nSettingsRequest, - AgentI18nContext, - I18nConfigResponse, - SupportedLanguagesResponse, - TimezonesResponse, -) -from ...config.database import get_db -from ...services.i18n.i18n_service import I18nService -from ...config.logging import get_logger - -logger = get_logger(__name__) - -router = APIRouter(prefix="/i18n", tags=["i18n"]) - - -# Dependency to get i18n service -def get_i18n_service(db: Session = Depends(get_db)) -> I18nService: - """Get i18n service instance.""" - return I18nService(db) - - -@router.get("/config", response_model=SuccessResponse) -async def get_config( - user_id: Optional[str] = Header(None, alias="X-User-ID"), - session_id: Optional[str] = Header(None, alias="X-Session-ID"), - i18n_service: I18nService = Depends(get_i18n_service), -) -> SuccessResponse: - """Get i18n configuration for user.""" - try: - config = await i18n_service.get_user_config(user_id) - return SuccessResponse( - message="I18n configuration retrieved successfully", - data=config - ) - except Exception as e: - logger.error(f"Error getting i18n config: {e}", exc_info=True) - raise HTTPException(status_code=500, detail="Internal server error") - - -@router.get("/languages", response_model=SuccessResponse) -async def get_supported_languages( - i18n_service: I18nService = Depends(get_i18n_service), -) -> SuccessResponse: - """Get supported languages.""" - try: - languages = await i18n_service.get_supported_languages() - return SuccessResponse( - message="Supported languages retrieved successfully", - data=languages - ) - except Exception as e: - logger.error(f"Error getting supported languages: {e}", exc_info=True) - raise HTTPException(status_code=500, detail="Internal server error") - - -@router.post("/language", response_model=SuccessResponse) -async def set_language( - request: LanguageRequest, - user_id: Optional[str] = Header(None, alias="X-User-ID"), - i18n_service: I18nService = Depends(get_i18n_service), -) -> SuccessResponse: - """Set user language preference.""" - try: - result = await i18n_service.set_user_language(user_id, request.language) - return SuccessResponse( - message=f"Language set to {request.language}", - data=result - ) - except ValueError as e: - raise HTTPException(status_code=400, detail=str(e)) - except Exception as e: - logger.error(f"Error setting language: {e}", exc_info=True) - raise HTTPException(status_code=500, detail="Internal server error") - - -@router.get("/timezones", response_model=SuccessResponse) -async def get_timezones( - i18n_service: I18nService = Depends(get_i18n_service), -) -> SuccessResponse: - """Get supported timezones.""" - try: - timezones = await i18n_service.get_supported_timezones() - return SuccessResponse( - message="Supported timezones retrieved successfully", - data=timezones - ) - except Exception as e: - logger.error(f"Error getting timezones: {e}", exc_info=True) - raise HTTPException(status_code=500, detail="Internal server error") - - -@router.post("/timezone", response_model=SuccessResponse) -async def set_timezone( - request: TimezoneRequest, - user_id: Optional[str] = Header(None, alias="X-User-ID"), - i18n_service: I18nService = Depends(get_i18n_service), -) -> SuccessResponse: - """Set user timezone preference.""" - try: - result = await i18n_service.set_user_timezone(user_id, request.timezone) - return SuccessResponse( - message=f"Timezone set to {request.timezone}", - data=result - ) - except ValueError as e: - raise HTTPException(status_code=400, detail=str(e)) - except Exception as e: - logger.error(f"Error setting timezone: {e}", exc_info=True) - raise HTTPException(status_code=500, detail="Internal server error") - - -@router.post("/detect-language", response_model=SuccessResponse) -async def detect_language( - request: LanguageDetectionRequest, - i18n_service: I18nService = Depends(get_i18n_service), -) -> SuccessResponse: - """Detect language from Accept-Language header.""" - try: - detected = await i18n_service.detect_language(request.accept_language) - return SuccessResponse( - message="Language detected successfully", - data=detected - ) - except Exception as e: - logger.error(f"Error detecting language: {e}", exc_info=True) - raise HTTPException(status_code=500, detail="Internal server error") - - -@router.post("/translate", response_model=SuccessResponse) -async def translate( - request: TranslationRequest, - i18n_service: I18nService = Depends(get_i18n_service), -) -> SuccessResponse: - """Translate a key to target language.""" - try: - translation = await i18n_service.translate( - key=request.key, - language=request.language, - variables=request.variables - ) - return SuccessResponse( - message="Translation retrieved successfully", - data={"translation": translation} - ) - except Exception as e: - logger.error(f"Error translating: {e}", exc_info=True) - raise HTTPException(status_code=500, detail="Internal server error") - - -@router.post("/format/datetime", response_model=SuccessResponse) -async def format_datetime( - request: DateTimeFormatRequest, - user_id: Optional[str] = Header(None, alias="X-User-ID"), - i18n_service: I18nService = Depends(get_i18n_service), -) -> SuccessResponse: - """Format datetime according to user preferences.""" - try: - formatted = await i18n_service.format_datetime( - datetime_str=request.datetime, - format_type=request.format_type, - user_id=user_id - ) - return SuccessResponse( - message="DateTime formatted successfully", - data={"formatted": formatted} - ) - except Exception as e: - logger.error(f"Error formatting datetime: {e}", exc_info=True) - raise HTTPException(status_code=500, detail="Internal server error") - - -@router.post("/format/number", response_model=SuccessResponse) -async def format_number( - request: NumberFormatRequest, - user_id: Optional[str] = Header(None, alias="X-User-ID"), - i18n_service: I18nService = Depends(get_i18n_service), -) -> SuccessResponse: - """Format number according to user preferences.""" - try: - formatted = await i18n_service.format_number( - number=request.number, - decimal_places=request.decimal_places, - user_id=user_id - ) - return SuccessResponse( - message="Number formatted successfully", - data={"formatted": formatted} - ) - except Exception as e: - logger.error(f"Error formatting number: {e}", exc_info=True) - raise HTTPException(status_code=500, detail="Internal server error") - - -@router.post("/format/currency", response_model=SuccessResponse) -async def format_currency( - request: CurrencyFormatRequest, - user_id: Optional[str] = Header(None, alias="X-User-ID"), - i18n_service: I18nService = Depends(get_i18n_service), -) -> SuccessResponse: - """Format currency according to user preferences.""" - try: - formatted = await i18n_service.format_currency( - amount=request.amount, - decimal_places=request.decimal_places, - user_id=user_id - ) - return SuccessResponse( - message="Currency formatted successfully", - data={"formatted": formatted} - ) - except Exception as e: - logger.error(f"Error formatting currency: {e}", exc_info=True) - raise HTTPException(status_code=500, detail="Internal server error") - - -@router.get("/user/settings", response_model=SuccessResponse) -async def get_user_settings( - user_id: str = Header(..., alias="X-User-ID"), - i18n_service: I18nService = Depends(get_i18n_service), -) -> SuccessResponse: - """Get user i18n settings.""" - try: - settings = await i18n_service.get_user_settings(user_id) - return SuccessResponse( - message="User settings retrieved successfully", - data=settings - ) - except Exception as e: - logger.error(f"Error getting user settings: {e}", exc_info=True) - raise HTTPException(status_code=500, detail="Internal server error") - - -@router.put("/user/settings", response_model=SuccessResponse) -async def update_user_settings( - request: UserI18nSettingsRequest, - user_id: str = Header(..., alias="X-User-ID"), - i18n_service: I18nService = Depends(get_i18n_service), -) -> SuccessResponse: - """Update user i18n settings.""" - try: - settings = await i18n_service.update_user_settings( - user_id=user_id, - language=request.language, - timezone=request.timezone - ) - return SuccessResponse( - message="User settings updated successfully", - data=settings - ) - except ValueError as e: - raise HTTPException(status_code=400, detail=str(e)) - except Exception as e: - logger.error(f"Error updating user settings: {e}", exc_info=True) - raise HTTPException(status_code=500, detail="Internal server error") - - -@router.get("/agent/context", response_model=SuccessResponse) -async def get_agent_context( - user_id: Optional[str] = Header(None, alias="X-User-ID"), - session_id: Optional[str] = Header(None, alias="X-Session-ID"), - i18n_service: I18nService = Depends(get_i18n_service), -) -> SuccessResponse: - """Get i18n context for agent execution.""" - try: - context = await i18n_service.get_agent_context(user_id, session_id) - return SuccessResponse( - message="Agent context retrieved successfully", - data=context - ) - except Exception as e: - logger.error(f"Error getting agent context: {e}", exc_info=True) - raise HTTPException(status_code=500, detail="Internal server error") \ No newline at end of file diff --git a/python/valuecell/server/services/agents/__init__.py b/python/valuecell/server/services/agents/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/python/valuecell/server/services/agents/agent_service.py b/python/valuecell/server/services/agents/agent_service.py deleted file mode 100644 index fe19d7549..000000000 --- a/python/valuecell/server/services/agents/agent_service.py +++ /dev/null @@ -1,137 +0,0 @@ -"""Agent service for ValueCell Server.""" - -import time -from typing import List, Optional, Dict, Any -from sqlalchemy.orm import Session -from ...db.repositories.agent_repository import AgentRepository -from ...db.models.agent import Agent -from ...api.schemas.agents import ( - AgentCreateRequest, - AgentUpdateRequest, - AgentExecutionRequest, - AgentExecutionResponse, -) -from ...config.logging import get_logger - -logger = get_logger(__name__) - - -class AgentService: - """Service for managing agents.""" - - def __init__(self, db: Session): - """Initialize agent service.""" - self.db = db - self.repository = AgentRepository(db) - - async def list_agents( - self, - skip: int = 0, - limit: int = 100, - agent_type: Optional[str] = None, - is_active: Optional[bool] = None, - ) -> List[Agent]: - """List agents with optional filtering.""" - logger.info(f"Listing agents: skip={skip}, limit={limit}, type={agent_type}") - - filters = {} - if agent_type: - filters["agent_type"] = agent_type - if is_active is not None: - filters["is_active"] = is_active - - return await self.repository.list_agents( - skip=skip, - limit=limit, - filters=filters - ) - - async def get_agent(self, agent_id: str) -> Optional[Agent]: - """Get agent by ID.""" - logger.info(f"Getting agent: {agent_id}") - return await self.repository.get_agent(agent_id) - - async def create_agent(self, agent_data: AgentCreateRequest) -> Agent: - """Create a new agent.""" - logger.info(f"Creating agent: {agent_data.name}") - - agent = Agent( - name=agent_data.name, - description=agent_data.description, - agent_type=agent_data.agent_type, - config=agent_data.config, - is_active=agent_data.is_active, - ) - - return await self.repository.create_agent(agent) - - async def update_agent( - self, - agent_id: str, - agent_data: AgentUpdateRequest - ) -> Optional[Agent]: - """Update an existing agent.""" - logger.info(f"Updating agent: {agent_id}") - - agent = await self.repository.get_agent(agent_id) - if not agent: - return None - - # Update fields - update_data = agent_data.dict(exclude_unset=True) - for field, value in update_data.items(): - setattr(agent, field, value) - - return await self.repository.update_agent(agent) - - async def delete_agent(self, agent_id: str) -> bool: - """Delete an agent.""" - logger.info(f"Deleting agent: {agent_id}") - return await self.repository.delete_agent(agent_id) - - async def execute_agent( - self, - agent_id: str, - execution_request: AgentExecutionRequest - ) -> AgentExecutionResponse: - """Execute an agent with given parameters.""" - logger.info(f"Executing agent: {agent_id}") - - agent = await self.repository.get_agent(agent_id) - if not agent: - raise ValueError(f"Agent {agent_id} not found") - - if not agent.is_active: - raise ValueError(f"Agent {agent_id} is not active") - - try: - # TODO: Implement actual agent execution logic - # This would integrate with the existing agent execution framework - - # For now, return a mock response - result = { - "status": "completed", - "message": f"Agent {agent.name} executed successfully", - "data": execution_request.parameters, - } - - logger.info(f"Agent execution completed: {agent_id}") - - return AgentExecutionResponse( - agent_id=agent_id, - execution_id=f"exec_{agent_id}_{int(time.time())}", - status="completed", - result=result, - error=None, - ) - - except Exception as e: - logger.error(f"Agent execution failed: {agent_id}", exc_info=True) - - return AgentExecutionResponse( - agent_id=agent_id, - execution_id=f"exec_{agent_id}_{int(time.time())}", - status="failed", - result=None, - error=str(e), - ) \ No newline at end of file diff --git a/python/valuecell/server/services/assets/__init__.py b/python/valuecell/server/services/assets/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/python/valuecell/server/services/assets/asset_service.py b/python/valuecell/server/services/assets/asset_service.py deleted file mode 100644 index 69946b8bb..000000000 --- a/python/valuecell/server/services/assets/asset_service.py +++ /dev/null @@ -1,160 +0,0 @@ -"""Asset service for ValueCell Server.""" - -from typing import List, Optional, Dict, Any -from datetime import datetime, timedelta -from decimal import Decimal -from sqlalchemy.orm import Session -from ...db.repositories.asset_repository import AssetRepository -from ...db.models.asset import Asset -from ...api.schemas.assets import ( - AssetSearchRequest, - AssetResponse, - AssetPriceResponse, - PricePoint, -) -from ...config.logging import get_logger - -logger = get_logger(__name__) - - -class AssetService: - """Service for managing assets and market data.""" - - def __init__(self, db: Session): - """Initialize asset service.""" - self.db = db - self.repository = AssetRepository(db) - - async def search_assets( - self, - search_request: AssetSearchRequest - ) -> List[Asset]: - """Search assets based on query and filters.""" - logger.info(f"Searching assets: {search_request.query}") - - filters = {} - if search_request.asset_types: - filters["asset_type"] = search_request.asset_types - if search_request.exchanges: - filters["exchange"] = search_request.exchanges - - return await self.repository.search_assets( - query=search_request.query, - filters=filters, - limit=search_request.limit - ) - - async def get_asset(self, asset_id: str) -> Optional[Asset]: - """Get asset by ID.""" - logger.info(f"Getting asset: {asset_id}") - return await self.repository.get_asset(asset_id) - - async def get_asset_by_symbol(self, symbol: str) -> Optional[Asset]: - """Get asset by symbol.""" - logger.info(f"Getting asset by symbol: {symbol}") - return await self.repository.get_asset_by_symbol(symbol) - - async def list_assets( - self, - skip: int = 0, - limit: int = 100, - asset_type: Optional[str] = None, - exchange: Optional[str] = None, - is_active: Optional[bool] = None, - ) -> List[Asset]: - """List assets with optional filtering.""" - logger.info(f"Listing assets: skip={skip}, limit={limit}") - - filters = {} - if asset_type: - filters["asset_type"] = asset_type - if exchange: - filters["exchange"] = exchange - if is_active is not None: - filters["is_active"] = is_active - - return await self.repository.list_assets( - skip=skip, - limit=limit, - filters=filters - ) - - async def get_asset_price( - self, - symbol: str, - period: str = "1d", - interval: str = "1h" - ) -> Optional[AssetPriceResponse]: - """Get current and historical price data for an asset.""" - logger.info(f"Getting price data for: {symbol}") - - asset = await self.get_asset_by_symbol(symbol) - if not asset: - return None - - try: - # TODO: Integrate with actual market data provider - # This would connect to APIs like Alpha Vantage, Yahoo Finance, etc. - - # For now, return mock data - current_price = Decimal("150.00") - price_change = Decimal("2.50") - price_change_percent = (price_change / (current_price - price_change)) * 100 - - # Generate mock historical data - historical_data = [] - base_time = datetime.now() - timedelta(days=1) - - for i in range(24): # 24 hours of hourly data - timestamp = base_time + timedelta(hours=i) - price_variation = Decimal(str(147 + (i * 0.5))) - - historical_data.append(PricePoint( - timestamp=timestamp, - open=price_variation, - high=price_variation + Decimal("1.0"), - low=price_variation - Decimal("1.0"), - close=price_variation + Decimal("0.5"), - volume=1000000 + (i * 10000) - )) - - return AssetPriceResponse( - symbol=symbol, - current_price=current_price, - price_change=price_change, - price_change_percent=price_change_percent, - period=period, - historical_data=historical_data, - last_updated=datetime.now() - ) - - except Exception as e: - logger.error(f"Failed to get price data for {symbol}", exc_info=True) - return None - - async def update_asset_cache(self, symbol: str) -> bool: - """Update cached market data for an asset.""" - logger.info(f"Updating asset cache: {symbol}") - - try: - # TODO: Implement cache update logic - # This would fetch latest data and update cache/database - return True - - except Exception as e: - logger.error(f"Failed to update cache for {symbol}", exc_info=True) - return False - - async def get_trending_assets(self, limit: int = 10) -> List[Asset]: - """Get trending assets based on volume or price movement.""" - logger.info(f"Getting trending assets: limit={limit}") - - # TODO: Implement trending logic based on actual market data - # For now, return most recently updated assets - return await self.repository.list_assets( - skip=0, - limit=limit, - filters={"is_active": True}, - order_by="updated_at", - order_desc=True - ) \ No newline at end of file diff --git a/python/valuecell/server/services/auth/__init__.py b/python/valuecell/server/services/auth/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/python/valuecell/server/services/i18n/__init__.py b/python/valuecell/server/services/i18n/__init__.py deleted file mode 100644 index 1310d0040..000000000 --- a/python/valuecell/server/services/i18n/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -"""I18n services for ValueCell Server.""" - -from .i18n_service import I18nService - -__all__ = ["I18nService"] \ No newline at end of file diff --git a/python/valuecell/server/services/i18n/i18n_service.py b/python/valuecell/server/services/i18n/i18n_service.py deleted file mode 100644 index d29633a5a..000000000 --- a/python/valuecell/server/services/i18n/i18n_service.py +++ /dev/null @@ -1,311 +0,0 @@ -"""I18n service for ValueCell Server.""" - -import json -from typing import Dict, Any, Optional, List -from datetime import datetime -from sqlalchemy.orm import Session -from pathlib import Path - -from ...config.logging import get_logger -from ...config.settings import get_settings - -logger = get_logger(__name__) - - -class I18nService: - """Service for handling internationalization.""" - - def __init__(self, db: Session): - """Initialize i18n service.""" - self.db = db - self.settings = get_settings() - self._translations = {} - self._load_translations() - - def _load_translations(self) -> None: - """Load translation files.""" - try: - # Define supported languages - self.supported_languages = { - "en-US": {"name": "English (US)", "native_name": "English"}, - "zh-Hans": {"name": "Chinese (Simplified)", "native_name": "简体中文"}, - "zh-Hant": {"name": "Chinese (Traditional)", "native_name": "繁體中文"}, - "ja-JP": {"name": "Japanese", "native_name": "日本語"}, - "ko-KR": {"name": "Korean", "native_name": "한국어"}, - } - - # Define supported timezones - self.supported_timezones = [ - "UTC", - "America/New_York", - "America/Los_Angeles", - "Europe/London", - "Europe/Paris", - "Asia/Shanghai", - "Asia/Tokyo", - "Asia/Seoul", - "Australia/Sydney", - ] - - # Load basic translations (in production, these would come from files) - self._translations = { - "en-US": { - "welcome": "Welcome to ValueCell", - "error.not_found": "Resource not found", - "error.internal": "Internal server error", - "success.created": "Resource created successfully", - "success.updated": "Resource updated successfully", - "success.deleted": "Resource deleted successfully", - }, - "zh-Hans": { - "welcome": "欢迎使用ValueCell", - "error.not_found": "资源未找到", - "error.internal": "内部服务器错误", - "success.created": "资源创建成功", - "success.updated": "资源更新成功", - "success.deleted": "资源删除成功", - }, - "zh-Hant": { - "welcome": "歡迎使用ValueCell", - "error.not_found": "資源未找到", - "error.internal": "內部伺服器錯誤", - "success.created": "資源創建成功", - "success.updated": "資源更新成功", - "success.deleted": "資源刪除成功", - }, - } - - logger.info("Translations loaded successfully") - - except Exception as e: - logger.error(f"Error loading translations: {e}") - # Fallback to minimal English translations - self._translations = { - "en-US": { - "welcome": "Welcome to ValueCell", - "error.not_found": "Resource not found", - "error.internal": "Internal server error", - } - } - - async def get_supported_languages(self) -> Dict[str, Any]: - """Get list of supported languages.""" - return { - "languages": self.supported_languages, - "default": self.settings.DEFAULT_LANGUAGE - } - - async def get_supported_timezones(self) -> Dict[str, Any]: - """Get list of supported timezones.""" - return { - "timezones": self.supported_timezones, - "default": self.settings.DEFAULT_TIMEZONE - } - - async def get_user_config(self, user_id: Optional[str]) -> Dict[str, Any]: - """Get i18n configuration for user.""" - # In a real implementation, this would fetch from database - # For now, return default configuration - return { - "language": self.settings.DEFAULT_LANGUAGE, - "timezone": self.settings.DEFAULT_TIMEZONE, - "date_format": "YYYY-MM-DD", - "time_format": "HH:mm:ss", - "currency": "USD", - "number_format": { - "decimal_separator": ".", - "thousands_separator": ",", - "decimal_places": 2 - } - } - - async def set_user_language(self, user_id: Optional[str], language: str) -> Dict[str, Any]: - """Set user language preference.""" - if language not in self.supported_languages: - raise ValueError(f"Unsupported language: {language}") - - # In a real implementation, this would save to database - logger.info(f"Setting language to {language} for user {user_id}") - - return { - "language": language, - "message": f"Language set to {language}" - } - - async def set_user_timezone(self, user_id: Optional[str], timezone: str) -> Dict[str, Any]: - """Set user timezone preference.""" - if timezone not in self.supported_timezones: - raise ValueError(f"Unsupported timezone: {timezone}") - - # In a real implementation, this would save to database - logger.info(f"Setting timezone to {timezone} for user {user_id}") - - return { - "timezone": timezone, - "message": f"Timezone set to {timezone}" - } - - async def detect_language(self, accept_language: str) -> Dict[str, Any]: - """Detect language from Accept-Language header.""" - # Simple language detection logic - # In production, use a proper language detection library - - languages = accept_language.lower().split(",") - detected_language = self.settings.DEFAULT_LANGUAGE - - for lang in languages: - lang = lang.strip().split(";")[0] # Remove quality values - if lang in self.supported_languages: - detected_language = lang - break - # Check for language family matches - elif lang.startswith("zh"): - detected_language = "zh-Hans" - break - elif lang.startswith("en"): - detected_language = "en-US" - break - - return { - "detected_language": detected_language, - "confidence": 0.8, # Mock confidence score - "supported": detected_language in self.supported_languages - } - - async def translate(self, key: str, language: str, variables: Optional[Dict[str, Any]] = None) -> str: - """Translate a key to target language.""" - if language not in self.supported_languages: - language = self.settings.DEFAULT_LANGUAGE - - translations = self._translations.get(language, self._translations.get(self.settings.DEFAULT_LANGUAGE, {})) - translation = translations.get(key, key) - - # Simple variable substitution - if variables: - for var_key, var_value in variables.items(): - translation = translation.replace(f"{{{var_key}}}", str(var_value)) - - return translation - - async def format_datetime(self, datetime_str: str, format_type: str, user_id: Optional[str] = None) -> str: - """Format datetime according to user preferences.""" - try: - # Parse datetime string - dt = datetime.fromisoformat(datetime_str.replace("Z", "+00:00")) - - # Get user config (in real implementation, from database) - config = await self.get_user_config(user_id) - - # Format based on type - if format_type == "date": - return dt.strftime("%Y-%m-%d") - elif format_type == "time": - return dt.strftime("%H:%M:%S") - elif format_type == "datetime": - return dt.strftime("%Y-%m-%d %H:%M:%S") - else: - return dt.isoformat() - - except Exception as e: - logger.error(f"Error formatting datetime: {e}") - return datetime_str - - async def format_number(self, number: float, decimal_places: Optional[int] = None, user_id: Optional[str] = None) -> str: - """Format number according to user preferences.""" - try: - config = await self.get_user_config(user_id) - number_format = config.get("number_format", {}) - - if decimal_places is None: - decimal_places = number_format.get("decimal_places", 2) - - # Format number - formatted = f"{number:,.{decimal_places}f}" - - # Apply user preferences - decimal_sep = number_format.get("decimal_separator", ".") - thousands_sep = number_format.get("thousands_separator", ",") - - if decimal_sep != ".": - formatted = formatted.replace(".", "__DECIMAL__") - if thousands_sep != ",": - formatted = formatted.replace(",", thousands_sep) - if decimal_sep != ".": - formatted = formatted.replace("__DECIMAL__", decimal_sep) - - return formatted - - except Exception as e: - logger.error(f"Error formatting number: {e}") - return str(number) - - async def format_currency(self, amount: float, decimal_places: Optional[int] = None, user_id: Optional[str] = None) -> str: - """Format currency according to user preferences.""" - try: - config = await self.get_user_config(user_id) - currency = config.get("currency", "USD") - - # Format as number first - formatted_number = await self.format_number(amount, decimal_places, user_id) - - # Add currency symbol - currency_symbols = { - "USD": "$", - "EUR": "€", - "GBP": "£", - "JPY": "¥", - "CNY": "¥", - "KRW": "₩" - } - - symbol = currency_symbols.get(currency, currency) - return f"{symbol}{formatted_number}" - - except Exception as e: - logger.error(f"Error formatting currency: {e}") - return str(amount) - - async def get_user_settings(self, user_id: str) -> Dict[str, Any]: - """Get user i18n settings.""" - # In real implementation, fetch from database - return await self.get_user_config(user_id) - - async def update_user_settings(self, user_id: str, language: Optional[str] = None, timezone: Optional[str] = None) -> Dict[str, Any]: - """Update user i18n settings.""" - settings = {} - - if language: - if language not in self.supported_languages: - raise ValueError(f"Unsupported language: {language}") - settings["language"] = language - - if timezone: - if timezone not in self.supported_timezones: - raise ValueError(f"Unsupported timezone: {timezone}") - settings["timezone"] = timezone - - # In real implementation, save to database - logger.info(f"Updated settings for user {user_id}: {settings}") - - # Return updated config - config = await self.get_user_config(user_id) - config.update(settings) - return config - - async def get_agent_context(self, user_id: Optional[str], session_id: Optional[str]) -> Dict[str, Any]: - """Get i18n context for agent execution.""" - config = await self.get_user_config(user_id) - - return { - "user_id": user_id, - "session_id": session_id, - "language": config["language"], - "timezone": config["timezone"], - "locale_info": { - "date_format": config["date_format"], - "time_format": config["time_format"], - "currency": config["currency"], - "number_format": config["number_format"] - }, - "translations": self._translations.get(config["language"], {}) - } \ No newline at end of file diff --git a/python/valuecell/services/__init__.py b/python/valuecell/services/__init__.py index b5f1f42cd..9b4c6b348 100644 --- a/python/valuecell/services/__init__.py +++ b/python/valuecell/services/__init__.py @@ -1,23 +1,14 @@ """ValueCell Services Module. This module provides high-level service layers for various business operations -including asset management, internationalization, and agent context management. +including 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/services/agent_context.py index a82ba59b7..94bd82530 100644 --- a/python/valuecell/services/agent_context.py +++ b/python/valuecell/services/agent_context.py @@ -5,8 +5,6 @@ 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 @@ -15,23 +13,15 @@ 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): """Set current user context for the agent.""" - user_context = self.i18n_api.get_user_context(user_id) - - # Store in thread local storage + # Store in thread local storage with default values 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") - - # Update i18n service - self.i18n_service.set_language(self._local.language) - self.i18n_service.set_timezone(self._local.timezone) + self._local.language = "en-US" # Default language + self._local.timezone = "UTC" # Default timezone def get_current_user_id(self) -> Optional[str]: """Get current user ID.""" @@ -54,31 +44,33 @@ def get_i18n_context(self) -> AgentI18nContext: return AgentI18nContext( language=self.get_current_language(), timezone=self.get_current_timezone(), - currency_symbol=self.i18n_service._i18n_config.get_currency_symbol(), - date_format=self.i18n_service._i18n_config.get_date_format(), - time_format=self.i18n_service._i18n_config.get_time_format(), - number_format=self.i18n_service._i18n_config.get_number_format(), + currency_symbol="$", # Default currency symbol + date_format="YYYY-MM-DD", # Default date format + time_format="HH:mm:ss", # Default time format + number_format="en-US", # Default number format user_id=self.get_current_user_id(), session_id=self.get_current_session_id(), ) def translate(self, key: str, **variables) -> str: """Translate using current user's language.""" - return self.i18n_service.translate( - key, self.get_current_language(), **variables - ) + # i18n service removed, return key as fallback + return key def format_datetime(self, dt: datetime, format_type: str = "datetime") -> str: """Format datetime using current user's settings.""" - return self.i18n_service.format_datetime(dt, format_type) + # i18n service removed, return basic format + return dt.isoformat() def format_number(self, number: float, decimal_places: int = 2) -> str: """Format number using current user's settings.""" - return self.i18n_service.format_number(number, decimal_places) + # i18n service removed, return basic format + return f"{number:.{decimal_places}f}" def format_currency(self, amount: float, decimal_places: int = 2) -> str: """Format currency using current user's settings.""" - return self.i18n_service.format_currency(amount, decimal_places) + # i18n service removed, return basic format + return f"${amount:.{decimal_places}f}" @contextmanager def user_context(self, user_id: str, session_id: Optional[str] = None): diff --git a/python/valuecell/services/application/__init__.py b/python/valuecell/services/application/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/python/valuecell/services/assets/__init__.py b/python/valuecell/services/assets/__init__.py deleted file mode 100644 index 18873f983..000000000 --- a/python/valuecell/services/assets/__init__.py +++ /dev/null @@ -1,51 +0,0 @@ -"""ValueCell Asset Service Module. - -This module provides high-level asset service functionality for financial asset management, -search, price retrieval, and watchlist operations with internationalization support. - -Key Features: -- Asset search with localization -- Real-time and historical price data -- Watchlist management -- Multi-language support -- Integration with multiple data adapters - -Usage Example: - ```python - from valuecell.services.assets import ( - get_asset_service, search_assets, add_to_watchlist - ) - - # Search for assets - results = search_assets("AAPL", language="zh-Hans") - - # Add to watchlist - add_to_watchlist(user_id="user123", ticker="NASDAQ:AAPL") - ``` -""" - -from .asset_service import ( - AssetService, - get_asset_service, - reset_asset_service, - search_assets, - get_asset_info, - get_asset_price, - add_to_watchlist, - get_watchlist, -) - -__version__ = "1.0.0" - -__all__ = [ - # Service class - "AssetService", - "get_asset_service", - "reset_asset_service", - # Convenience functions - "search_assets", - "get_asset_info", - "get_asset_price", - "add_to_watchlist", - "get_watchlist", -] diff --git a/python/valuecell/services/assets/asset_service.py b/python/valuecell/services/assets/asset_service.py deleted file mode 100644 index 1e3093f1c..000000000 --- a/python/valuecell/services/assets/asset_service.py +++ /dev/null @@ -1,636 +0,0 @@ -"""Asset service for asset management and watchlist operations. - -This module provides high-level service functions for asset search, watchlist management, -and price data retrieval with i18n support. -""" - -import logging -from typing import Dict, List, Optional, Any -from datetime import datetime - -from ...adapters.assets.manager import get_adapter_manager, get_watchlist_manager -from ...adapters.assets.i18n_integration import get_asset_i18n_service -from ...adapters.assets.types import AssetSearchQuery, AssetType -from ...config.i18n import get_i18n_config - -logger = logging.getLogger(__name__) - - -class AssetService: - """High-level service for asset operations with i18n support.""" - - def __init__(self): - """Initialize asset service.""" - self.adapter_manager = get_adapter_manager() - self.watchlist_manager = get_watchlist_manager() - self.i18n_service = get_asset_i18n_service() - - def search_assets( - self, - query: str, - asset_types: Optional[List[str]] = None, - exchanges: Optional[List[str]] = None, - countries: Optional[List[str]] = None, - limit: int = 50, - language: Optional[str] = None, - ) -> Dict[str, Any]: - """Search for assets with localization support. - - Args: - query: Search query string - asset_types: Filter by asset types (optional) - exchanges: Filter by exchanges (optional) - countries: Filter by countries (optional) - limit: Maximum number of results - language: Language for localized results - - Returns: - Dictionary containing search results and metadata - """ - try: - # Convert string asset types to enum - parsed_asset_types = None - if asset_types: - parsed_asset_types = [] - for asset_type_str in asset_types: - try: - parsed_asset_types.append(AssetType(asset_type_str.lower())) - except ValueError: - logger.warning(f"Invalid asset type: {asset_type_str}") - - # Create search query - search_query = AssetSearchQuery( - query=query, - asset_types=parsed_asset_types, - exchanges=exchanges, - countries=countries, - limit=limit, - language=language or get_i18n_config().language, - ) - - # Perform search - results = self.adapter_manager.search_assets(search_query) - - # Localize results - localized_results = self.i18n_service.localize_search_results( - results, language - ) - - # Convert to dictionary format - result_dicts = [] - for result in localized_results: - result_dict = { - "ticker": result.ticker, - "asset_type": result.asset_type.value, - "asset_type_display": self.i18n_service.get_asset_type_display_name( - result.asset_type, language - ), - "names": result.names, - "display_name": result.get_display_name( - language or get_i18n_config().language - ), - "exchange": result.exchange, - "country": result.country, - "currency": result.currency, - "market_status": result.market_status.value, - "market_status_display": self.i18n_service.get_market_status_display_name( - result.market_status, language - ), - "relevance_score": result.relevance_score, - } - result_dicts.append(result_dict) - - return { - "success": True, - "results": result_dicts, - "count": len(result_dicts), - "query": query, - "filters": { - "asset_types": asset_types, - "exchanges": exchanges, - "countries": countries, - "limit": limit, - }, - "language": language or get_i18n_config().language, - } - - except Exception as e: - logger.error(f"Error searching assets: {e}") - return {"success": False, "error": str(e), "results": [], "count": 0} - - def get_asset_info( - self, ticker: str, language: Optional[str] = None - ) -> Dict[str, Any]: - """Get detailed asset information with localization. - - Args: - ticker: Asset ticker in internal format - language: Language for localized content - - Returns: - Dictionary containing asset information - """ - try: - asset = self.adapter_manager.get_asset_info(ticker) - - if not asset: - return {"success": False, "error": "Asset not found", "ticker": ticker} - - # Localize asset - localized_asset = self.i18n_service.localize_asset(asset, language) - - # Convert to dictionary - asset_dict = { - "success": True, - "ticker": localized_asset.ticker, - "asset_type": localized_asset.asset_type.value, - "asset_type_display": self.i18n_service.get_asset_type_display_name( - localized_asset.asset_type, language - ), - "names": localized_asset.names.names, - "display_name": localized_asset.get_localized_name( - language or get_i18n_config().language - ), - "descriptions": localized_asset.descriptions, - "market_info": { - "exchange": localized_asset.market_info.exchange, - "country": localized_asset.market_info.country, - "currency": localized_asset.market_info.currency, - "timezone": localized_asset.market_info.timezone, - "trading_hours": localized_asset.market_info.trading_hours, - "market_status": localized_asset.market_info.market_status.value, - }, - "source_mappings": { - k.value: v for k, v in localized_asset.source_mappings.items() - }, - "properties": localized_asset.properties, - "created_at": localized_asset.created_at.isoformat(), - "updated_at": localized_asset.updated_at.isoformat(), - "is_active": localized_asset.is_active, - } - - return asset_dict - - except Exception as e: - logger.error(f"Error getting asset info for {ticker}: {e}") - return {"success": False, "error": str(e), "ticker": ticker} - - def get_asset_price( - self, ticker: str, language: Optional[str] = None - ) -> Dict[str, Any]: - """Get current price for an asset with localized formatting. - - Args: - ticker: Asset ticker in internal format - language: Language for localized formatting - - Returns: - Dictionary containing price information - """ - try: - price_data = self.adapter_manager.get_real_time_price(ticker) - - if not price_data: - return { - "success": False, - "error": "Price data not available", - "ticker": ticker, - } - - # Format price data with localization - formatted_price = { - "success": True, - "ticker": price_data.ticker, - "price": float(price_data.price), - "price_formatted": self.i18n_service.format_currency_amount( - float(price_data.price), price_data.currency, language - ), - "currency": price_data.currency, - "timestamp": price_data.timestamp.isoformat(), - "volume": float(price_data.volume) if price_data.volume else None, - "open_price": float(price_data.open_price) - if price_data.open_price - else None, - "high_price": float(price_data.high_price) - if price_data.high_price - else None, - "low_price": float(price_data.low_price) - if price_data.low_price - else None, - "close_price": float(price_data.close_price) - if price_data.close_price - else None, - "change": float(price_data.change) if price_data.change else None, - "change_percent": float(price_data.change_percent) - if price_data.change_percent - else None, - "change_percent_formatted": self.i18n_service.format_percentage_change( - float(price_data.change_percent), language - ) - if price_data.change_percent - else None, - "market_cap": float(price_data.market_cap) - if price_data.market_cap - else None, - "market_cap_formatted": self.i18n_service.format_market_cap( - float(price_data.market_cap), price_data.currency, language - ) - if price_data.market_cap - else None, - "source": price_data.source.value if price_data.source else None, - } - - return formatted_price - - except Exception as e: - logger.error(f"Error getting price for {ticker}: {e}") - return {"success": False, "error": str(e), "ticker": ticker} - - def get_multiple_prices( - self, tickers: List[str], language: Optional[str] = None - ) -> Dict[str, Any]: - """Get prices for multiple assets efficiently. - - Args: - tickers: List of asset tickers - language: Language for localized formatting - - Returns: - Dictionary containing price data for all tickers - """ - try: - price_data = self.adapter_manager.get_multiple_prices(tickers) - - formatted_prices = {} - - for ticker, price in price_data.items(): - if price: - formatted_prices[ticker] = { - "price": float(price.price), - "price_formatted": self.i18n_service.format_currency_amount( - float(price.price), price.currency, language - ), - "currency": price.currency, - "timestamp": price.timestamp.isoformat(), - "change": float(price.change) if price.change else None, - "change_percent": float(price.change_percent) - if price.change_percent - else None, - "change_percent_formatted": self.i18n_service.format_percentage_change( - float(price.change_percent), language - ) - if price.change_percent - else None, - "volume": float(price.volume) if price.volume else None, - "market_cap": float(price.market_cap) - if price.market_cap - else None, - "market_cap_formatted": self.i18n_service.format_market_cap( - float(price.market_cap), price.currency, language - ) - if price.market_cap - else None, - "source": price.source.value if price.source else None, - } - else: - formatted_prices[ticker] = None - - return { - "success": True, - "prices": formatted_prices, - "count": len([p for p in formatted_prices.values() if p is not None]), - "requested_count": len(tickers), - } - - except Exception as e: - logger.error(f"Error getting multiple prices: {e}") - return {"success": False, "error": str(e), "prices": {}} - - def create_watchlist( - self, - user_id: str, - name: str = "My Watchlist", - description: str = "", - is_default: bool = False, - ) -> Dict[str, Any]: - """Create a new watchlist for a user. - - Args: - user_id: User identifier - name: Watchlist name - description: Watchlist description - is_default: Whether this is the default watchlist - - Returns: - Dictionary containing created watchlist information - """ - try: - watchlist = self.watchlist_manager.create_watchlist( - user_id, name, description, is_default - ) - - return { - "success": True, - "watchlist": { - "user_id": watchlist.user_id, - "name": watchlist.name, - "description": watchlist.description, - "created_at": watchlist.created_at.isoformat(), - "updated_at": watchlist.updated_at.isoformat(), - "is_default": watchlist.is_default, - "is_public": watchlist.is_public, - "items_count": len(watchlist.items), - }, - } - - except Exception as e: - logger.error(f"Error creating watchlist: {e}") - return {"success": False, "error": str(e)} - - def add_to_watchlist( - self, - user_id: str, - ticker: str, - watchlist_name: Optional[str] = None, - notes: str = "", - ) -> Dict[str, Any]: - """Add an asset to a watchlist. - - Args: - user_id: User identifier - ticker: Asset ticker to add - watchlist_name: Watchlist name (uses default if None) - notes: User notes about the asset - - Returns: - Dictionary containing operation result - """ - try: - success = self.watchlist_manager.add_asset_to_watchlist( - user_id, ticker, watchlist_name, notes - ) - - if success: - return { - "success": True, - "message": "Asset added to watchlist successfully", - "ticker": ticker, - "user_id": user_id, - "watchlist_name": watchlist_name, - } - else: - return { - "success": False, - "error": "Failed to add asset to watchlist", - "ticker": ticker, - } - - except Exception as e: - logger.error(f"Error adding {ticker} to watchlist: {e}") - return {"success": False, "error": str(e), "ticker": ticker} - - def remove_from_watchlist( - self, user_id: str, ticker: str, watchlist_name: Optional[str] = None - ) -> Dict[str, Any]: - """Remove an asset from a watchlist. - - Args: - user_id: User identifier - ticker: Asset ticker to remove - watchlist_name: Watchlist name (uses default if None) - - Returns: - Dictionary containing operation result - """ - try: - success = self.watchlist_manager.remove_asset_from_watchlist( - user_id, ticker, watchlist_name - ) - - if success: - return { - "success": True, - "message": "Asset removed from watchlist successfully", - "ticker": ticker, - "user_id": user_id, - "watchlist_name": watchlist_name, - } - else: - return { - "success": False, - "error": "Asset not found in watchlist or watchlist not found", - "ticker": ticker, - } - - except Exception as e: - logger.error(f"Error removing {ticker} from watchlist: {e}") - return {"success": False, "error": str(e), "ticker": ticker} - - def get_watchlist( - self, - user_id: str, - watchlist_name: Optional[str] = None, - include_prices: bool = True, - language: Optional[str] = None, - ) -> Dict[str, Any]: - """Get watchlist with asset information and prices. - - Args: - user_id: User identifier - watchlist_name: Watchlist name (uses default if None) - include_prices: Whether to include current prices - language: Language for localized content - - Returns: - Dictionary containing watchlist data - """ - try: - # Get watchlist - if watchlist_name: - watchlist = self.watchlist_manager.get_watchlist( - user_id, watchlist_name - ) - else: - watchlist = self.watchlist_manager.get_default_watchlist(user_id) - - if not watchlist: - return { - "success": False, - "error": "Watchlist not found", - "user_id": user_id, - "watchlist_name": watchlist_name, - } - - # Get asset information and prices - assets_data = [] - tickers = watchlist.get_tickers() - - # Get prices if requested - prices_data = {} - if include_prices and tickers: - prices_result = self.get_multiple_prices(tickers, language) - if prices_result["success"]: - prices_data = prices_result["prices"] - - # Build asset data - for item in sorted(watchlist.items, key=lambda x: x.order): - asset_data = { - "ticker": item.ticker, - "display_name": self.i18n_service.get_localized_asset_name( - item.ticker, language - ), - "added_at": item.added_at.isoformat(), - "order": item.order, - "notes": item.notes, - "alerts": item.alerts, - } - - # Add price data if available - if item.ticker in prices_data and prices_data[item.ticker]: - asset_data["price_data"] = prices_data[item.ticker] - - assets_data.append(asset_data) - - return { - "success": True, - "watchlist": { - "user_id": watchlist.user_id, - "name": watchlist.name, - "description": watchlist.description, - "created_at": watchlist.created_at.isoformat(), - "updated_at": watchlist.updated_at.isoformat(), - "is_default": watchlist.is_default, - "is_public": watchlist.is_public, - "items_count": len(watchlist.items), - "assets": assets_data, - }, - } - - except Exception as e: - logger.error(f"Error getting watchlist: {e}") - return {"success": False, "error": str(e), "user_id": user_id} - - def get_user_watchlists(self, user_id: str) -> Dict[str, Any]: - """Get all watchlists for a user. - - Args: - user_id: User identifier - - Returns: - Dictionary containing all user watchlists - """ - try: - watchlists = self.watchlist_manager.get_user_watchlists(user_id) - - watchlists_data = [] - for watchlist in watchlists: - watchlist_data = { - "name": watchlist.name, - "description": watchlist.description, - "created_at": watchlist.created_at.isoformat(), - "updated_at": watchlist.updated_at.isoformat(), - "is_default": watchlist.is_default, - "is_public": watchlist.is_public, - "items_count": len(watchlist.items), - } - watchlists_data.append(watchlist_data) - - return { - "success": True, - "user_id": user_id, - "watchlists": watchlists_data, - "count": len(watchlists_data), - } - - except Exception as e: - logger.error(f"Error getting user watchlists: {e}") - return {"success": False, "error": str(e), "user_id": user_id} - - def get_system_health(self) -> Dict[str, Any]: - """Get system health status for all data adapters. - - Returns: - Dictionary containing health status for all adapters - """ - try: - health_data = self.adapter_manager.health_check() - - # Convert enum keys to strings - health_status = {} - for source, status in health_data.items(): - health_status[source.value] = status - - # Calculate overall health - healthy_count = sum( - 1 - for status in health_status.values() - if status.get("status") == "healthy" - ) - total_count = len(health_status) - - # Determine overall status - if total_count == 0: - overall_status = "no_adapters" - elif healthy_count == total_count: - overall_status = "healthy" - elif healthy_count > 0: - overall_status = "degraded" - else: - overall_status = "unhealthy" - - return { - "success": True, - "overall_status": overall_status, - "healthy_adapters": healthy_count, - "total_adapters": total_count, - "adapters": health_status, - "timestamp": datetime.utcnow().isoformat(), - } - - except Exception as e: - logger.error(f"Error getting system health: {e}") - return {"success": False, "error": str(e), "overall_status": "error"} - - -# Global service instance -_asset_service: Optional[AssetService] = None - - -def get_asset_service() -> AssetService: - """Get global asset service instance.""" - global _asset_service - if _asset_service is None: - _asset_service = AssetService() - return _asset_service - - -def reset_asset_service() -> None: - """Reset global asset service instance (mainly for testing).""" - global _asset_service - _asset_service = None - - -# Convenience functions for direct service access -def search_assets(query: str, **kwargs) -> Dict[str, Any]: - """Convenience function for asset search.""" - return get_asset_service().search_assets(query, **kwargs) - - -def get_asset_info(ticker: str, **kwargs) -> Dict[str, Any]: - """Convenience function for getting asset info.""" - return get_asset_service().get_asset_info(ticker, **kwargs) - - -def get_asset_price(ticker: str, **kwargs) -> Dict[str, Any]: - """Convenience function for getting asset price.""" - return get_asset_service().get_asset_price(ticker, **kwargs) - - -def add_to_watchlist(user_id: str, ticker: str, **kwargs) -> Dict[str, Any]: - """Convenience function for adding to watchlist.""" - return get_asset_service().add_to_watchlist(user_id, ticker, **kwargs) - - -def get_watchlist(user_id: str, **kwargs) -> Dict[str, Any]: - """Convenience function for getting watchlist.""" - return get_asset_service().get_watchlist(user_id, **kwargs) diff --git a/python/valuecell/services/i18n_service.py b/python/valuecell/services/i18n_service.py deleted file mode 100644 index e3cdd2cff..000000000 --- a/python/valuecell/services/i18n_service.py +++ /dev/null @@ -1,325 +0,0 @@ -"""Internationalization service for ValueCell application.""" - -import json -from pathlib import Path -from typing import Dict, Any, Optional, List -from datetime import datetime - -from ..config.settings import get_settings -from ..config.i18n import get_i18n_config -from ..core.constants import SUPPORTED_LANGUAGE_CODES, DEFAULT_LANGUAGE - - -class TranslationManager: - """Manages translation loading and caching.""" - - def __init__(self, locale_dir: Optional[Path] = None): - """Initialize translation manager. - - Args: - locale_dir: Directory containing translation files - """ - self._locale_dir = locale_dir or get_settings().LOCALE_DIR - self._translations: Dict[str, Dict[str, Any]] = {} - self._load_all_translations() - - def _load_all_translations(self) -> None: - """Load all translation files.""" - for lang_code in SUPPORTED_LANGUAGE_CODES: - self._load_translation(lang_code) - - def _load_translation(self, language: str) -> None: - """Load translation for specific language. - - Args: - language: Language code to load - """ - translation_file = self._locale_dir / f"{language}.json" - - if translation_file.exists(): - try: - with open(translation_file, "r", encoding="utf-8") as f: - self._translations[language] = json.load(f) - except (json.JSONDecodeError, IOError) as e: - print(f"Error loading translation file {translation_file}: {e}") - self._translations[language] = {} - else: - # Create empty translation if file doesn't exist - self._translations[language] = {} - - def get_translation(self, language: str, key: str, **kwargs) -> str: - """Get translated string for given key and language. - - Args: - language: Language code - key: Translation key (supports dot notation for nested keys) - **kwargs: Variables for string formatting - - Returns: - Translated string or key if translation not found - """ - if language not in self._translations: - language = DEFAULT_LANGUAGE - - translations = self._translations.get(language, {}) - - # Support dot notation for nested keys - keys = key.split(".") - value = translations - - try: - for k in keys: - value = value[k] - except (KeyError, TypeError): - # Fallback to default language - if language != DEFAULT_LANGUAGE: - return self.get_translation(DEFAULT_LANGUAGE, key, **kwargs) - return key # Return key if no translation found - - # Format string with provided variables - if isinstance(value, str) and kwargs: - try: - return value.format(**kwargs) - except (KeyError, ValueError): - return value - - return str(value) - - def reload_translations(self) -> None: - """Reload all translation files.""" - self._translations.clear() - self._load_all_translations() - - def get_available_keys(self, language: str) -> List[str]: - """Get all available translation keys for a language. - - Args: - language: Language code - - Returns: - List of available translation keys - """ - translations = self._translations.get(language, {}) - - def _get_keys(obj: Dict[str, Any], prefix: str = "") -> List[str]: - keys = [] - for key, value in obj.items(): - full_key = f"{prefix}.{key}" if prefix else key - if isinstance(value, dict): - keys.extend(_get_keys(value, full_key)) - else: - keys.append(full_key) - return keys - - return _get_keys(translations) - - -class I18nService: - """Main internationalization service.""" - - def __init__(self): - """Initialize i18n service.""" - self._translation_manager = TranslationManager() - self._i18n_config = get_i18n_config() - - def translate(self, key: str, language: Optional[str] = None, **kwargs) -> str: - """Translate a key to current or specified language. - - Args: - key: Translation key - language: Target language (uses current if not specified) - **kwargs: Variables for string formatting - - Returns: - Translated string - """ - target_language = language or self._i18n_config.language - return self._translation_manager.get_translation(target_language, key, **kwargs) - - def t(self, key: str, **kwargs) -> str: - """Short alias for translate method. - - Args: - key: Translation key - **kwargs: Variables for string formatting - - Returns: - Translated string - """ - return self.translate(key, **kwargs) - - def get_current_language(self) -> str: - """Get current language code.""" - return self._i18n_config.language - - def get_current_timezone(self) -> str: - """Get current timezone.""" - return self._i18n_config.timezone - - def set_language(self, language: str) -> bool: - """Set current language. - - Args: - language: Language code to set - - Returns: - True if language was set successfully - """ - if language in SUPPORTED_LANGUAGE_CODES: - self._i18n_config.set_language(language) - get_settings().update_language(language) - return True - return False - - def set_timezone(self, timezone: str) -> bool: - """Set current timezone. - - Args: - timezone: Timezone to set - - Returns: - True if timezone was set successfully - """ - try: - self._i18n_config.set_timezone(timezone) - get_settings().update_timezone(timezone) - return True - except Exception: - return False - - def format_datetime(self, dt: datetime, format_type: str = "datetime") -> str: - """Format datetime according to current language settings. - - Args: - dt: Datetime to format - format_type: Type of format ('date', 'time', 'datetime') - - Returns: - Formatted datetime string - """ - return self._i18n_config.format_datetime(dt, format_type) - - def format_number(self, number: float, decimal_places: int = 2) -> str: - """Format number according to current language settings. - - Args: - number: Number to format - decimal_places: Number of decimal places - - Returns: - Formatted number string - """ - return self._i18n_config.format_number(number, decimal_places) - - def format_currency(self, amount: float, decimal_places: int = 2) -> str: - """Format currency according to current language settings. - - Args: - amount: Amount to format - decimal_places: Number of decimal places - - Returns: - Formatted currency string - """ - return self._i18n_config.format_currency(amount, decimal_places) - - def get_supported_languages(self) -> List[tuple]: - """Get list of supported languages. - - Returns: - List of (code, name) tuples - """ - from ..core.constants import SUPPORTED_LANGUAGES - - return SUPPORTED_LANGUAGES - - def get_language_name(self, language_code: str) -> str: - """Get display name for language code. - - Args: - language_code: Language code - - Returns: - Display name or code if not found - """ - from ..core.constants import SUPPORTED_LANGUAGES - - for code, name in SUPPORTED_LANGUAGES: - if code == language_code: - return name - return language_code - - def reload_translations(self) -> None: - """Reload all translation files.""" - self._translation_manager.reload_translations() - - def get_translation_keys(self, language: Optional[str] = None) -> List[str]: - """Get all available translation keys for a language. - - Args: - language: Language code (uses current if not specified) - - Returns: - List of available translation keys - """ - target_language = language or self._i18n_config.language - return self._translation_manager.get_available_keys(target_language) - - def to_dict(self) -> Dict[str, Any]: - """Get current i18n configuration as dictionary. - - Returns: - Dictionary with i18n configuration - """ - 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(), - } - - -# Global i18n service instance -_i18n_service: Optional[I18nService] = None - - -def get_i18n_service() -> I18nService: - """Get global i18n service instance.""" - global _i18n_service - if _i18n_service is None: - _i18n_service = I18nService() - return _i18n_service - - -def reset_i18n_service() -> None: - """Reset global i18n service instance.""" - global _i18n_service - _i18n_service = None - - -# Convenience functions -def t(key: str, **kwargs) -> str: - """Translate a key (convenience function). - - Args: - key: Translation key - **kwargs: Variables for string formatting - - Returns: - Translated string - """ - return get_i18n_service().translate(key, **kwargs) - - -def translate(key: str, language: Optional[str] = None, **kwargs) -> str: - """Translate a key to specified language (convenience function). - - Args: - key: Translation key - language: Target language - **kwargs: Variables for string formatting - - Returns: - Translated string - """ - return get_i18n_service().translate(key, language, **kwargs) diff --git a/python/valuecell/utils/i18n_utils.py b/python/valuecell/utils/i18n_utils.py deleted file mode 100644 index e3cd657bf..000000000 --- a/python/valuecell/utils/i18n_utils.py +++ /dev/null @@ -1,426 +0,0 @@ -"""Internationalization utility functions for ValueCell application.""" - -import re -from typing import Dict, List, Optional, Any -from datetime import datetime -import pytz -from pathlib import Path - -from ..core.constants import ( - SUPPORTED_LANGUAGE_CODES, - LANGUAGE_TIMEZONE_MAPPING, - DEFAULT_LANGUAGE, - DEFAULT_TIMEZONE, - SUPPORTED_LANGUAGES, -) -from ..services.i18n_service import get_i18n_service - - -def detect_browser_language(accept_language_header: str) -> str: - """Detect preferred language from browser Accept-Language header. - - Args: - accept_language_header: HTTP Accept-Language header value - - Returns: - Best matching supported language code - """ - if not accept_language_header: - return DEFAULT_LANGUAGE - - # Parse Accept-Language header - languages = [] - for item in accept_language_header.split(","): - parts = item.strip().split(";") - lang = parts[0].strip() - - # Extract quality value - quality = 1.0 - if len(parts) > 1: - q_part = parts[1].strip() - if q_part.startswith("q="): - try: - quality = float(q_part[2:]) - except ValueError: - quality = 1.0 - - languages.append((lang, quality)) - - # Sort by quality (descending) - languages.sort(key=lambda x: x[1], reverse=True) - - # Find best match - for lang, _ in languages: - # Direct match - if lang in SUPPORTED_LANGUAGE_CODES: - return lang - - # Try to match language family (e.g., 'zh' -> 'zh-Hans') - lang_family = lang.split("-")[0] - for supported_lang in SUPPORTED_LANGUAGE_CODES: - if supported_lang.startswith(lang_family): - return supported_lang - - return DEFAULT_LANGUAGE - - -def get_timezone_for_language(language: str) -> str: - """Get default timezone for a language. - - Args: - language: Language code - - Returns: - Timezone string - """ - return LANGUAGE_TIMEZONE_MAPPING.get(language, DEFAULT_TIMEZONE) - - -def validate_language_code(language: str) -> bool: - """Validate if language code is supported. - - Args: - language: Language code to validate - - Returns: - True if language is supported - """ - return language in SUPPORTED_LANGUAGE_CODES - - -def validate_timezone(timezone_str: str) -> bool: - """Validate if timezone string is valid. - - Args: - timezone_str: Timezone string to validate - - Returns: - True if timezone is valid - """ - try: - pytz.timezone(timezone_str) - return True - except pytz.UnknownTimeZoneError: - return False - - -def get_available_timezones() -> List[str]: - """Get list of all available timezones. - - Returns: - List of timezone strings - """ - return sorted(pytz.all_timezones) - - -def get_common_timezones() -> List[str]: - """Get list of commonly used timezones. - - Returns: - List of common timezone strings - """ - return sorted(pytz.common_timezones) - - -def get_timezone_display_name(timezone_str: str) -> str: - """Get display name for timezone. - - Args: - timezone_str: Timezone string - - Returns: - Human-readable timezone name - """ - try: - tz = pytz.timezone(timezone_str) - now = datetime.now(tz) - return f"{timezone_str} (UTC{now.strftime('%z')})" - except pytz.UnknownTimeZoneError: - return timezone_str - - -def convert_timezone(dt: datetime, from_tz: str, to_tz: str) -> datetime: - """Convert datetime from one timezone to another. - - Args: - dt: Datetime to convert - from_tz: Source timezone - to_tz: Target timezone - - Returns: - Converted datetime - """ - try: - from_timezone = pytz.timezone(from_tz) - to_timezone = pytz.timezone(to_tz) - - # Localize if naive - if dt.tzinfo is None: - dt = from_timezone.localize(dt) - - # Convert to target timezone - return dt.astimezone(to_timezone) - except pytz.UnknownTimeZoneError: - return dt - - -def format_file_size(size_bytes: int, language: Optional[str] = None) -> str: - """Format file size according to language settings. - - Args: - size_bytes: File size in bytes - language: Language code (uses current if not specified) - - Returns: - Formatted file size string - """ - i18n = get_i18n_service() - target_language = language or i18n.get_current_language() - - if size_bytes == 0: - return f"0 {i18n.translate('units.bytes', language=target_language)}" - - units = ["bytes", "kb", "mb", "gb", "tb"] - size = float(size_bytes) - unit_index = 0 - - while size >= 1024 and unit_index < len(units) - 1: - size /= 1024 - unit_index += 1 - - unit_key = f"units.{units[unit_index]}" - unit_name = i18n.translate(unit_key, language=target_language) - - if unit_index == 0: - return f"{int(size)} {unit_name}" - else: - formatted_size = i18n.format_number(size, 1) - return f"{formatted_size} {unit_name}" - - -def format_duration(seconds: int, language: Optional[str] = None) -> str: - """Format duration according to language settings. - - Args: - seconds: Duration in seconds - language: Language code (uses current if not specified) - - Returns: - Formatted duration string - """ - i18n = get_i18n_service() - target_language = language or i18n.get_current_language() - - if seconds < 60: - unit_name = i18n.translate("units.seconds", language=target_language) - return f"{seconds} {unit_name}" - elif seconds < 3600: - minutes = seconds // 60 - unit_name = i18n.translate("units.minutes", language=target_language) - return f"{minutes} {unit_name}" - elif seconds < 86400: - hours = seconds // 3600 - unit_name = i18n.translate("units.hours", language=target_language) - return f"{hours} {unit_name}" - else: - days = seconds // 86400 - unit_name = i18n.translate("units.days", language=target_language) - return f"{days} {unit_name}" - - -def pluralize( - count: int, - singular: str, - plural: Optional[str] = None, - language: Optional[str] = None, -) -> str: - """Pluralize a word based on count and language rules. - - Args: - count: Number to determine plural form - singular: Singular form of the word - plural: Plural form (auto-generated if not provided) - language: Language code (uses current if not specified) - - Returns: - Appropriate word form - """ - target_language = language or get_i18n_service().get_current_language() - - # Chinese languages don't have plural forms - if target_language.startswith("zh"): - return singular - - # English pluralization rules - if count == 1: - return singular - - if plural: - return plural - - # Simple English pluralization - if singular.endswith(("s", "sh", "ch", "x", "z")): - return f"{singular}es" - elif singular.endswith("y") and singular[-2] not in "aeiou": - return f"{singular[:-1]}ies" - elif singular.endswith("f"): - return f"{singular[:-1]}ves" - elif singular.endswith("fe"): - return f"{singular[:-2]}ves" - else: - return f"{singular}s" - - -def get_language_direction(language: str) -> str: - """Get text direction for a language. - - Args: - language: Language code - - Returns: - 'ltr' for left-to-right, 'rtl' for right-to-left - """ - # All currently supported languages are LTR - return "ltr" - - -def extract_translation_keys(text: str) -> List[str]: - """Extract translation keys from text using t() function calls. - - Args: - text: Text to extract keys from - - Returns: - List of translation keys found - """ - # Pattern to match t('key') or t("key") calls - pattern = r't\([\'"]([^\'"]+)[\'"]\)' - matches = re.findall(pattern, text) - return list(set(matches)) # Remove duplicates - - -def validate_translation_file(file_path: Path) -> Dict[str, Any]: - """Validate a translation JSON file. - - Args: - file_path: Path to translation file - - Returns: - Validation result with status and errors - """ - result = {"valid": True, "errors": [], "warnings": [], "key_count": 0} - - try: - import json - - if not file_path.exists(): - result["valid"] = False - result["errors"].append("File does not exist") - return result - - with open(file_path, "r", encoding="utf-8") as f: - data = json.load(f) - - # Count keys recursively - def count_keys(obj): - count = 0 - if isinstance(obj, dict): - for key, value in obj.items(): - if isinstance(value, dict): - count += count_keys(value) - else: - count += 1 - return count - - result["key_count"] = count_keys(data) - - # Check for empty values - def check_empty_values(obj, prefix=""): - for key, value in obj.items(): - current_key = f"{prefix}.{key}" if prefix else key - if isinstance(value, dict): - check_empty_values(value, current_key) - elif not value or (isinstance(value, str) and not value.strip()): - result["warnings"].append(f"Empty value for key: {current_key}") - - check_empty_values(data) - - except json.JSONDecodeError as e: - result["valid"] = False - result["errors"].append(f"Invalid JSON: {str(e)}") - except Exception as e: - result["valid"] = False - result["errors"].append(f"Error reading file: {str(e)}") - - return result - - -def get_missing_translations(base_language: str = "en-US") -> Dict[str, List[str]]: - """Find missing translations compared to base language. - - Args: - base_language: Base language to compare against - - Returns: - Dictionary with missing keys for each language - """ - i18n = get_i18n_service() - base_keys = set(i18n.get_translation_keys(base_language)) - missing = {} - - for lang_code, _ in SUPPORTED_LANGUAGES: - if lang_code == base_language: - continue - - lang_keys = set(i18n.get_translation_keys(lang_code)) - missing_keys = base_keys - lang_keys - - if missing_keys: - missing[lang_code] = sorted(list(missing_keys)) - - return missing - - -def create_translation_template(keys: List[str]) -> Dict[str, Any]: - """Create a translation template with given keys. - - Args: - keys: List of translation keys - - Returns: - Nested dictionary template - """ - template = {} - - for key in keys: - parts = key.split(".") - current = template - - for i, part in enumerate(parts): - if i == len(parts) - 1: - # Last part, set empty string - current[part] = "" - else: - # Create nested dict if doesn't exist - if part not in current: - current[part] = {} - current = current[part] - - return template - - -# Decorator for translatable strings -def translatable(key: str, **kwargs): - """Decorator to mark functions as translatable. - - Args: - key: Translation key - **kwargs: Additional translation parameters - """ - - def decorator(func): - func._translation_key = key - func._translation_params = kwargs - return func - - return decorator From 2f2ec49e251905bcc95fac7d5eaea577d9f82663 Mon Sep 17 00:00:00 2001 From: zhonghao lu Date: Mon, 15 Sep 2025 15:24:20 +0800 Subject: [PATCH 03/10] remove useless code --- .../valuecell/server/api/schemas/__init__.py | 65 ----- python/valuecell/server/api/schemas/agents.py | 104 -------- python/valuecell/server/api/schemas/assets.py | 125 ---------- python/valuecell/server/api/schemas/common.py | 28 --- python/valuecell/server/api/schemas/health.py | 22 -- python/valuecell/server/api/schemas/i18n.py | 149 ----------- .../db/migrations/001_initial_schema.py | 132 ---------- .../server/db/migrations/__init__.py | 0 python/valuecell/server/db/models/__init__.py | 7 +- python/valuecell/server/db/models/agent.py | 80 ------ python/valuecell/server/db/models/asset.py | 130 ---------- .../server/db/repositories/__init__.py | 9 - .../db/repositories/agent_repository.py | 161 ------------ .../db/repositories/asset_repository.py | 232 ------------------ 14 files changed, 1 insertion(+), 1243 deletions(-) delete mode 100644 python/valuecell/server/api/schemas/agents.py delete mode 100644 python/valuecell/server/api/schemas/assets.py delete mode 100644 python/valuecell/server/api/schemas/common.py delete mode 100644 python/valuecell/server/api/schemas/health.py delete mode 100644 python/valuecell/server/api/schemas/i18n.py delete mode 100644 python/valuecell/server/db/migrations/001_initial_schema.py delete mode 100644 python/valuecell/server/db/migrations/__init__.py delete mode 100644 python/valuecell/server/db/models/agent.py delete mode 100644 python/valuecell/server/db/models/asset.py delete mode 100644 python/valuecell/server/db/repositories/agent_repository.py delete mode 100644 python/valuecell/server/db/repositories/asset_repository.py diff --git a/python/valuecell/server/api/schemas/__init__.py b/python/valuecell/server/api/schemas/__init__.py index 5e19255ec..e69de29bb 100644 --- a/python/valuecell/server/api/schemas/__init__.py +++ b/python/valuecell/server/api/schemas/__init__.py @@ -1,65 +0,0 @@ -"""API schemas for ValueCell Server.""" - -from .common import BaseResponse, ErrorResponse, SuccessResponse -from .health import HealthResponse -from .agents import ( - AgentResponse, - AgentCreateRequest, - AgentUpdateRequest, - AgentExecutionRequest, - AgentExecutionResponse, -) -from .assets import ( - AssetResponse, - AssetPriceResponse, - AssetSearchRequest, - PricePoint, -) -from .i18n import ( - I18nConfigResponse, - SupportedLanguage, - SupportedLanguagesResponse, - TimezoneInfo, - TimezonesResponse, - LanguageRequest, - TimezoneRequest, - LanguageDetectionRequest, - TranslationRequest, - DateTimeFormatRequest, - NumberFormatRequest, - CurrencyFormatRequest, - UserI18nSettings, - UserI18nSettingsRequest, - AgentI18nContext, -) - -__all__ = [ - "BaseResponse", - "ErrorResponse", - "SuccessResponse", - "HealthResponse", - "AgentResponse", - "AgentCreateRequest", - "AgentUpdateRequest", - "AgentExecutionRequest", - "AgentExecutionResponse", - "AssetResponse", - "AssetPriceResponse", - "AssetSearchRequest", - "PricePoint", - "I18nConfigResponse", - "SupportedLanguage", - "SupportedLanguagesResponse", - "TimezoneInfo", - "TimezonesResponse", - "LanguageRequest", - "TimezoneRequest", - "LanguageDetectionRequest", - "TranslationRequest", - "DateTimeFormatRequest", - "NumberFormatRequest", - "CurrencyFormatRequest", - "UserI18nSettings", - "UserI18nSettingsRequest", - "AgentI18nContext", -] \ No newline at end of file diff --git a/python/valuecell/server/api/schemas/agents.py b/python/valuecell/server/api/schemas/agents.py deleted file mode 100644 index 38afb052c..000000000 --- a/python/valuecell/server/api/schemas/agents.py +++ /dev/null @@ -1,104 +0,0 @@ -"""Agent schemas for ValueCell Server.""" - -from typing import Dict, Any, Optional, List -from datetime import datetime -from pydantic import BaseModel, Field - - -class AgentBase(BaseModel): - """Base agent model.""" - - name: str = Field(..., description="Agent name") - description: str = Field(..., description="Agent description") - agent_type: str = Field(..., description="Type of agent (e.g., 'sec_13f', 'calculator')") - config: Dict[str, Any] = Field(default_factory=dict, description="Agent configuration") - is_active: bool = Field(True, description="Whether the agent is active") - - -class AgentCreateRequest(AgentBase): - """Request model for creating an agent.""" - pass - - -class AgentUpdateRequest(BaseModel): - """Request model for updating an agent.""" - - name: Optional[str] = Field(None, description="Agent name") - description: Optional[str] = Field(None, description="Agent description") - config: Optional[Dict[str, Any]] = Field(None, description="Agent configuration") - is_active: Optional[bool] = Field(None, description="Whether the agent is active") - - -class AgentResponse(AgentBase): - """Response model for agent data.""" - - id: str = Field(..., description="Agent ID") - created_at: datetime = Field(..., description="Creation timestamp") - updated_at: datetime = Field(..., description="Last update timestamp") - - class Config: - from_attributes = True - schema_extra = { - "example": { - "id": "agent_123", - "name": "SEC 13F Analyzer", - "description": "Analyzes SEC 13F filings for institutional holdings", - "agent_type": "sec_13f", - "config": { - "model": "gpt-4", - "temperature": 0.7 - }, - "is_active": True, - "created_at": "2024-01-01T00:00:00Z", - "updated_at": "2024-01-01T00:00:00Z" - } - } - - -class AgentExecutionRequest(BaseModel): - """Request model for agent execution.""" - - input_data: Dict[str, Any] = Field(..., description="Input data for the agent") - parameters: Optional[Dict[str, Any]] = Field(default_factory=dict, description="Execution parameters") - - class Config: - schema_extra = { - "example": { - "input_data": { - "query": "Analyze Berkshire Hathaway's latest 13F filing", - "ticker": "BRK.A" - }, - "parameters": { - "streaming": True, - "timeout": 300 - } - } - } - - -class AgentExecutionResponse(BaseModel): - """Response model for agent execution.""" - - execution_id: str = Field(..., description="Execution ID") - agent_id: str = Field(..., description="Agent ID") - status: str = Field(..., description="Execution status") - result: Optional[Dict[str, Any]] = Field(None, description="Execution result") - error: Optional[str] = Field(None, description="Error message if execution failed") - started_at: datetime = Field(..., description="Execution start time") - completed_at: Optional[datetime] = Field(None, description="Execution completion time") - - class Config: - schema_extra = { - "example": { - "execution_id": "exec_456", - "agent_id": "agent_123", - "status": "completed", - "result": { - "content": "Analysis of Berkshire Hathaway's 13F filing...", - "is_task_complete": True - }, - "error": None, - "started_at": "2024-01-01T00:00:00Z", - "completed_at": "2024-01-01T00:05:00Z" - } - } \ No newline at end of file diff --git a/python/valuecell/server/api/schemas/assets.py b/python/valuecell/server/api/schemas/assets.py deleted file mode 100644 index 38d9acd1d..000000000 --- a/python/valuecell/server/api/schemas/assets.py +++ /dev/null @@ -1,125 +0,0 @@ -"""Asset schemas for ValueCell Server.""" - -from typing import List, Optional, Dict, Any -from datetime import datetime -from decimal import Decimal -from pydantic import BaseModel, Field - - -class AssetBase(BaseModel): - """Base asset model.""" - - symbol: str = Field(..., description="Asset symbol (e.g., AAPL, BTC)") - name: str = Field(..., description="Asset name") - asset_type: str = Field(..., description="Asset type (stock, crypto, forex, etc.)") - exchange: Optional[str] = Field(None, description="Exchange where the asset is traded") - currency: str = Field("USD", description="Base currency") - - -class AssetResponse(AssetBase): - """Response model for asset data.""" - - id: str = Field(..., description="Asset ID") - market_cap: Optional[Decimal] = Field(None, description="Market capitalization") - sector: Optional[str] = Field(None, description="Asset sector") - industry: Optional[str] = Field(None, description="Asset industry") - description: Optional[str] = Field(None, description="Asset description") - is_active: bool = Field(True, description="Whether the asset is actively traded") - created_at: datetime = Field(..., description="Creation timestamp") - updated_at: datetime = Field(..., description="Last update timestamp") - - class Config: - from_attributes = True - schema_extra = { - "example": { - "id": "asset_123", - "symbol": "AAPL", - "name": "Apple Inc.", - "asset_type": "stock", - "exchange": "NASDAQ", - "currency": "USD", - "market_cap": "3000000000000", - "sector": "Technology", - "industry": "Consumer Electronics", - "description": "Apple Inc. designs, manufactures, and markets smartphones...", - "is_active": True, - "created_at": "2024-01-01T00:00:00Z", - "updated_at": "2024-01-01T00:00:00Z" - } - } - - -class PricePoint(BaseModel): - """Single price point model.""" - - timestamp: datetime = Field(..., description="Price timestamp") - open: Decimal = Field(..., description="Opening price") - high: Decimal = Field(..., description="Highest price") - low: Decimal = Field(..., description="Lowest price") - close: Decimal = Field(..., description="Closing price") - volume: Optional[int] = Field(None, description="Trading volume") - - class Config: - schema_extra = { - "example": { - "timestamp": "2024-01-01T00:00:00Z", - "open": "150.00", - "high": "155.00", - "low": "149.00", - "close": "154.00", - "volume": 1000000 - } - } - - -class AssetPriceResponse(BaseModel): - """Response model for asset price data.""" - - symbol: str = Field(..., description="Asset symbol") - current_price: Decimal = Field(..., description="Current price") - price_change: Decimal = Field(..., description="Price change from previous close") - price_change_percent: Decimal = Field(..., description="Price change percentage") - period: str = Field(..., description="Time period for historical data") - historical_data: List[PricePoint] = Field(..., description="Historical price data") - last_updated: datetime = Field(..., description="Last update timestamp") - - class Config: - schema_extra = { - "example": { - "symbol": "AAPL", - "current_price": "154.00", - "price_change": "2.50", - "price_change_percent": "1.65", - "period": "1d", - "historical_data": [ - { - "timestamp": "2024-01-01T00:00:00Z", - "open": "150.00", - "high": "155.00", - "low": "149.00", - "close": "154.00", - "volume": 1000000 - } - ], - "last_updated": "2024-01-01T16:00:00Z" - } - } - - -class AssetSearchRequest(BaseModel): - """Request model for asset search.""" - - query: str = Field(..., description="Search query") - asset_types: Optional[List[str]] = Field(None, description="Filter by asset types") - exchanges: Optional[List[str]] = Field(None, description="Filter by exchanges") - limit: int = Field(10, ge=1, le=100, description="Maximum number of results") - - class Config: - schema_extra = { - "example": { - "query": "Apple", - "asset_types": ["stock"], - "exchanges": ["NASDAQ"], - "limit": 10 - } - } \ No newline at end of file diff --git a/python/valuecell/server/api/schemas/common.py b/python/valuecell/server/api/schemas/common.py deleted file mode 100644 index 05c822a1b..000000000 --- a/python/valuecell/server/api/schemas/common.py +++ /dev/null @@ -1,28 +0,0 @@ -"""Common schemas for ValueCell Server.""" - -from typing import Dict, Any, Optional -from pydantic import BaseModel - - -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] \ No newline at end of file diff --git a/python/valuecell/server/api/schemas/health.py b/python/valuecell/server/api/schemas/health.py deleted file mode 100644 index 32c7812e3..000000000 --- a/python/valuecell/server/api/schemas/health.py +++ /dev/null @@ -1,22 +0,0 @@ -"""Health check schemas for ValueCell Server.""" - -from pydantic import BaseModel - - -class HealthResponse(BaseModel): - """Health check response model.""" - - status: str - version: str - environment: str - database: str - - class Config: - schema_extra = { - "example": { - "status": "healthy", - "version": "0.1.0", - "environment": "development", - "database": "healthy" - } - } \ No newline at end of file diff --git a/python/valuecell/server/api/schemas/i18n.py b/python/valuecell/server/api/schemas/i18n.py deleted file mode 100644 index 17deb0676..000000000 --- a/python/valuecell/server/api/schemas/i18n.py +++ /dev/null @@ -1,149 +0,0 @@ -"""I18n schemas for ValueCell Server.""" - -from typing import Dict, Any, List, Optional -from datetime import datetime -from pydantic import BaseModel, Field, validator - - -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 setting request.""" - - language: str = Field(..., description="Language code to set") - - @validator("language") - def validate_language(cls, v): - """Validate language code.""" - # TODO: Add proper validation logic - return v - - -class TimezoneRequest(BaseModel): - """Timezone setting 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 format request.""" - - datetime: str = Field(..., description="ISO datetime string") - format_type: str = Field( - "datetime", description="Format type: date, time, or datetime" - ) - - -class NumberFormatRequest(BaseModel): - """Number format request.""" - - number: float = Field(..., description="Number to format") - decimal_places: int = Field(2, description="Number of decimal places") - - -class CurrencyFormatRequest(BaseModel): - """Currency format request.""" - - amount: float = Field(..., description="Amount to format") - decimal_places: int = Field(2, description="Number of decimal places") - - -class UserI18nSettings(BaseModel): - """User i18n settings.""" - - 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): - """Validate language code.""" - # TODO: Add proper validation logic - 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): - """Validate language code.""" - if v is not None: - # TODO: Add proper validation logic - pass - return v - - -class AgentI18nContext(BaseModel): - """Agent i18n context.""" - - 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 \ No newline at end of file diff --git a/python/valuecell/server/db/migrations/001_initial_schema.py b/python/valuecell/server/db/migrations/001_initial_schema.py deleted file mode 100644 index 055335de5..000000000 --- a/python/valuecell/server/db/migrations/001_initial_schema.py +++ /dev/null @@ -1,132 +0,0 @@ -"""Initial database schema migration for ValueCell Server.""" - -from alembic import op -import sqlalchemy as sa -from sqlalchemy.dialects import postgresql - -# revision identifiers -revision = '001' -down_revision = None -branch_labels = None -depends_on = None - - -def upgrade(): - """Create initial database schema.""" - - # Create agents table - op.create_table( - 'agents', - sa.Column('id', sa.String(), nullable=False), - sa.Column('name', sa.String(length=255), nullable=False), - sa.Column('description', sa.Text(), nullable=True), - sa.Column('agent_type', sa.String(length=100), nullable=False), - sa.Column('config', sa.JSON(), nullable=False), - sa.Column('is_active', sa.Boolean(), nullable=False), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), - sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), - sa.Column('created_by', sa.String(length=255), nullable=True), - sa.Column('updated_by', sa.String(length=255), nullable=True), - sa.Column('execution_count', sa.Integer(), nullable=False), - sa.Column('last_executed_at', sa.DateTime(timezone=True), nullable=True), - sa.Column('average_execution_time', sa.Float(), nullable=True), - sa.PrimaryKeyConstraint('id') - ) - - # Create indexes for agents table - op.create_index(op.f('ix_agents_id'), 'agents', ['id'], unique=False) - op.create_index(op.f('ix_agents_name'), 'agents', ['name'], unique=False) - op.create_index(op.f('ix_agents_agent_type'), 'agents', ['agent_type'], unique=False) - op.create_index(op.f('ix_agents_is_active'), 'agents', ['is_active'], unique=False) - - # Create assets table - op.create_table( - 'assets', - sa.Column('id', sa.String(), nullable=False), - sa.Column('symbol', sa.String(length=20), nullable=False), - sa.Column('name', sa.String(length=255), nullable=False), - sa.Column('asset_type', sa.String(length=50), nullable=False), - sa.Column('exchange', sa.String(length=100), nullable=True), - sa.Column('currency', sa.String(length=10), nullable=False), - sa.Column('market_cap', sa.DECIMAL(precision=20, scale=2), nullable=True), - sa.Column('sector', sa.String(length=100), nullable=True), - sa.Column('industry', sa.String(length=100), nullable=True), - sa.Column('description', sa.Text(), nullable=True), - sa.Column('is_active', sa.Boolean(), nullable=False), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), - sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), - sa.Column('data_source', sa.String(length=100), nullable=True), - sa.Column('last_price_update', sa.DateTime(timezone=True), nullable=True), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('symbol') - ) - - # Create indexes for assets table - op.create_index(op.f('ix_assets_id'), 'assets', ['id'], unique=False) - op.create_index(op.f('ix_assets_symbol'), 'assets', ['symbol'], unique=True) - op.create_index(op.f('ix_assets_name'), 'assets', ['name'], unique=False) - op.create_index(op.f('ix_assets_asset_type'), 'assets', ['asset_type'], unique=False) - op.create_index(op.f('ix_assets_exchange'), 'assets', ['exchange'], unique=False) - op.create_index(op.f('ix_assets_sector'), 'assets', ['sector'], unique=False) - op.create_index(op.f('ix_assets_is_active'), 'assets', ['is_active'], unique=False) - - # Create asset_prices table - op.create_table( - 'asset_prices', - sa.Column('id', sa.String(), nullable=False), - sa.Column('asset_id', sa.String(), nullable=False), - sa.Column('symbol', sa.String(length=20), nullable=False), - sa.Column('timestamp', sa.DateTime(timezone=True), nullable=False), - sa.Column('open_price', sa.DECIMAL(precision=20, scale=8), nullable=False), - sa.Column('high_price', sa.DECIMAL(precision=20, scale=8), nullable=False), - sa.Column('low_price', sa.DECIMAL(precision=20, scale=8), nullable=False), - sa.Column('close_price', sa.DECIMAL(precision=20, scale=8), nullable=False), - sa.Column('volume', sa.Integer(), nullable=True), - sa.Column('adjusted_close', sa.DECIMAL(precision=20, scale=8), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), - sa.Column('data_source', sa.String(length=100), nullable=True), - sa.PrimaryKeyConstraint('id') - ) - - # Create indexes for asset_prices table - op.create_index(op.f('ix_asset_prices_id'), 'asset_prices', ['id'], unique=False) - op.create_index(op.f('ix_asset_prices_asset_id'), 'asset_prices', ['asset_id'], unique=False) - op.create_index(op.f('ix_asset_prices_symbol'), 'asset_prices', ['symbol'], unique=False) - op.create_index(op.f('ix_asset_prices_timestamp'), 'asset_prices', ['timestamp'], unique=False) - - # Create composite index for efficient price queries - op.create_index( - 'ix_asset_prices_symbol_timestamp', - 'asset_prices', - ['symbol', 'timestamp'], - unique=False - ) - - -def downgrade(): - """Drop all tables.""" - - # Drop indexes first - op.drop_index('ix_asset_prices_symbol_timestamp', table_name='asset_prices') - op.drop_index(op.f('ix_asset_prices_timestamp'), table_name='asset_prices') - op.drop_index(op.f('ix_asset_prices_symbol'), table_name='asset_prices') - op.drop_index(op.f('ix_asset_prices_asset_id'), table_name='asset_prices') - op.drop_index(op.f('ix_asset_prices_id'), table_name='asset_prices') - - op.drop_index(op.f('ix_assets_is_active'), table_name='assets') - op.drop_index(op.f('ix_assets_sector'), table_name='assets') - op.drop_index(op.f('ix_assets_exchange'), table_name='assets') - op.drop_index(op.f('ix_assets_asset_type'), table_name='assets') - op.drop_index(op.f('ix_assets_name'), table_name='assets') - op.drop_index(op.f('ix_assets_symbol'), table_name='assets') - op.drop_index(op.f('ix_assets_id'), table_name='assets') - - op.drop_index(op.f('ix_agents_is_active'), table_name='agents') - op.drop_index(op.f('ix_agents_agent_type'), table_name='agents') - op.drop_index(op.f('ix_agents_name'), table_name='agents') - op.drop_index(op.f('ix_agents_id'), table_name='agents') - - # Drop tables - op.drop_table('asset_prices') - op.drop_table('assets') - op.drop_table('agents') \ No newline at end of file diff --git a/python/valuecell/server/db/migrations/__init__.py b/python/valuecell/server/db/migrations/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/python/valuecell/server/db/models/__init__.py b/python/valuecell/server/db/models/__init__.py index f39ff5b79..e21a2308f 100644 --- a/python/valuecell/server/db/models/__init__.py +++ b/python/valuecell/server/db/models/__init__.py @@ -1,12 +1,7 @@ """Database models for ValueCell Server.""" from .base import Base -from .agent import Agent -from .asset import Asset, AssetPrice __all__ = [ - "Base", - "Agent", - "Asset", - "AssetPrice", + "Base" ] \ No newline at end of file diff --git a/python/valuecell/server/db/models/agent.py b/python/valuecell/server/db/models/agent.py deleted file mode 100644 index 67f873ed0..000000000 --- a/python/valuecell/server/db/models/agent.py +++ /dev/null @@ -1,80 +0,0 @@ -"""Agent model for ValueCell Server.""" - -from typing import Dict, Any, Optional -from datetime import datetime -from sqlalchemy import Column, String, Text, Boolean, DateTime, JSON, Integer, Float -from sqlalchemy.dialects.postgresql import UUID -from sqlalchemy.sql import func -import uuid -from .base import Base - - -class Agent(Base): - """Agent model for storing agent configurations and metadata.""" - - __tablename__ = "agents" - - id = Column( - String, - primary_key=True, - default=lambda: str(uuid.uuid4()), - index=True - ) - name = Column(String(255), nullable=False, index=True) - description = Column(Text, nullable=True) - agent_type = Column(String(100), nullable=False, index=True) - config = Column(JSON, nullable=False, default=dict) - is_active = Column(Boolean, default=True, nullable=False, index=True) - - # Metadata - created_at = Column( - DateTime(timezone=True), - server_default=func.now(), - nullable=False - ) - updated_at = Column( - DateTime(timezone=True), - server_default=func.now(), - onupdate=func.now(), - nullable=False - ) - created_by = Column(String(255), nullable=True) - updated_by = Column(String(255), nullable=True) - - # Performance tracking - execution_count = Column(Integer, default=0, nullable=False) - last_executed_at = Column(DateTime(timezone=True), nullable=True) - average_execution_time = Column(Float, nullable=True) - - def __repr__(self) -> str: - return f"" - - def to_dict(self) -> Dict[str, Any]: - """Convert agent to dictionary.""" - return { - "id": self.id, - "name": self.name, - "description": self.description, - "agent_type": self.agent_type, - "config": self.config, - "is_active": self.is_active, - "created_at": self.created_at.isoformat() if self.created_at else None, - "updated_at": self.updated_at.isoformat() if self.updated_at else None, - "created_by": self.created_by, - "updated_by": self.updated_by, - "execution_count": self.execution_count, - "last_executed_at": self.last_executed_at.isoformat() if self.last_executed_at else None, - "average_execution_time": self.average_execution_time, - } - - def update_execution_stats(self, execution_time: float) -> None: - """Update execution statistics.""" - self.execution_count += 1 - self.last_executed_at = datetime.utcnow() - - if self.average_execution_time is None: - self.average_execution_time = execution_time - else: - # Calculate running average - total_time = self.average_execution_time * (self.execution_count - 1) - self.average_execution_time = (total_time + execution_time) / self.execution_count \ No newline at end of file diff --git a/python/valuecell/server/db/models/asset.py b/python/valuecell/server/db/models/asset.py deleted file mode 100644 index 258127d30..000000000 --- a/python/valuecell/server/db/models/asset.py +++ /dev/null @@ -1,130 +0,0 @@ -"""Asset model for ValueCell Server.""" - -from typing import Dict, Any, Optional -from datetime import datetime -from decimal import Decimal -from sqlalchemy import Column, String, Text, Boolean, DateTime, DECIMAL, Integer -from sqlalchemy.sql import func -import uuid -from .base import Base - - -class Asset(Base): - """Asset model for storing financial instruments and market data.""" - - __tablename__ = "assets" - - id = Column( - String, - primary_key=True, - default=lambda: str(uuid.uuid4()), - index=True - ) - symbol = Column(String(20), nullable=False, unique=True, index=True) - name = Column(String(255), nullable=False, index=True) - asset_type = Column(String(50), nullable=False, index=True) # stock, crypto, forex, etc. - exchange = Column(String(100), nullable=True, index=True) - currency = Column(String(10), nullable=False, default="USD") - - # Market data - market_cap = Column(DECIMAL(20, 2), nullable=True) - sector = Column(String(100), nullable=True, index=True) - industry = Column(String(100), nullable=True, index=True) - description = Column(Text, nullable=True) - - # Status - is_active = Column(Boolean, default=True, nullable=False, index=True) - - # Metadata - created_at = Column( - DateTime(timezone=True), - server_default=func.now(), - nullable=False - ) - updated_at = Column( - DateTime(timezone=True), - server_default=func.now(), - onupdate=func.now(), - nullable=False - ) - - # Data source tracking - data_source = Column(String(100), nullable=True) # yahoo, alpha_vantage, etc. - last_price_update = Column(DateTime(timezone=True), nullable=True) - - def __repr__(self) -> str: - return f"" - - def to_dict(self) -> Dict[str, Any]: - """Convert asset to dictionary.""" - return { - "id": self.id, - "symbol": self.symbol, - "name": self.name, - "asset_type": self.asset_type, - "exchange": self.exchange, - "currency": self.currency, - "market_cap": str(self.market_cap) if self.market_cap else None, - "sector": self.sector, - "industry": self.industry, - "description": self.description, - "is_active": self.is_active, - "created_at": self.created_at.isoformat() if self.created_at else None, - "updated_at": self.updated_at.isoformat() if self.updated_at else None, - "data_source": self.data_source, - "last_price_update": self.last_price_update.isoformat() if self.last_price_update else None, - } - - -class AssetPrice(Base): - """Asset price model for storing historical price data.""" - - __tablename__ = "asset_prices" - - id = Column( - String, - primary_key=True, - default=lambda: str(uuid.uuid4()), - index=True - ) - asset_id = Column(String, nullable=False, index=True) - symbol = Column(String(20), nullable=False, index=True) - - # Price data - timestamp = Column(DateTime(timezone=True), nullable=False, index=True) - open_price = Column(DECIMAL(20, 8), nullable=False) - high_price = Column(DECIMAL(20, 8), nullable=False) - low_price = Column(DECIMAL(20, 8), nullable=False) - close_price = Column(DECIMAL(20, 8), nullable=False) - volume = Column(Integer, nullable=True) - - # Adjusted prices (for stocks) - adjusted_close = Column(DECIMAL(20, 8), nullable=True) - - # Metadata - created_at = Column( - DateTime(timezone=True), - server_default=func.now(), - nullable=False - ) - data_source = Column(String(100), nullable=True) - - def __repr__(self) -> str: - return f"" - - def to_dict(self) -> Dict[str, Any]: - """Convert asset price to dictionary.""" - return { - "id": self.id, - "asset_id": self.asset_id, - "symbol": self.symbol, - "timestamp": self.timestamp.isoformat() if self.timestamp else None, - "open": str(self.open_price), - "high": str(self.high_price), - "low": str(self.low_price), - "close": str(self.close_price), - "volume": self.volume, - "adjusted_close": str(self.adjusted_close) if self.adjusted_close else None, - "created_at": self.created_at.isoformat() if self.created_at else None, - "data_source": self.data_source, - } \ No newline at end of file diff --git a/python/valuecell/server/db/repositories/__init__.py b/python/valuecell/server/db/repositories/__init__.py index 4d3eb93d8..e69de29bb 100644 --- a/python/valuecell/server/db/repositories/__init__.py +++ b/python/valuecell/server/db/repositories/__init__.py @@ -1,9 +0,0 @@ -"""Database repositories for ValueCell Server.""" - -from .agent_repository import AgentRepository -from .asset_repository import AssetRepository - -__all__ = [ - "AgentRepository", - "AssetRepository", -] \ No newline at end of file diff --git a/python/valuecell/server/db/repositories/agent_repository.py b/python/valuecell/server/db/repositories/agent_repository.py deleted file mode 100644 index 99c1a988b..000000000 --- a/python/valuecell/server/db/repositories/agent_repository.py +++ /dev/null @@ -1,161 +0,0 @@ -"""Agent repository for ValueCell Server.""" - -from typing import List, Optional, Dict, Any -from sqlalchemy.orm import Session -from sqlalchemy import and_, or_ -from ..models.agent import Agent -from ...config.logging import get_logger - -logger = get_logger(__name__) - - -class AgentRepository: - """Repository for agent data access.""" - - def __init__(self, db: Session): - """Initialize agent repository.""" - self.db = db - - async def get_agent(self, agent_id: str) -> Optional[Agent]: - """Get agent by ID.""" - try: - return self.db.query(Agent).filter(Agent.id == agent_id).first() - except Exception as e: - logger.error(f"Error getting agent {agent_id}", exc_info=True) - return None - - async def get_agent_by_name(self, name: str) -> Optional[Agent]: - """Get agent by name.""" - try: - return self.db.query(Agent).filter(Agent.name == name).first() - except Exception as e: - logger.error(f"Error getting agent by name {name}", exc_info=True) - return None - - async def list_agents( - self, - skip: int = 0, - limit: int = 100, - filters: Optional[Dict[str, Any]] = None, - order_by: str = "created_at", - order_desc: bool = False, - ) -> List[Agent]: - """List agents with optional filtering and pagination.""" - try: - query = self.db.query(Agent) - - # Apply filters - if filters: - for key, value in filters.items(): - if hasattr(Agent, key): - if isinstance(value, list): - query = query.filter(getattr(Agent, key).in_(value)) - else: - query = query.filter(getattr(Agent, key) == value) - - # Apply ordering - if hasattr(Agent, order_by): - order_column = getattr(Agent, order_by) - if order_desc: - query = query.order_by(order_column.desc()) - else: - query = query.order_by(order_column) - - # Apply pagination - return query.offset(skip).limit(limit).all() - - except Exception as e: - logger.error(f"Error listing agents", exc_info=True) - return [] - - async def create_agent(self, agent: Agent) -> Agent: - """Create a new agent.""" - try: - self.db.add(agent) - self.db.commit() - self.db.refresh(agent) - logger.info(f"Created agent: {agent.id}") - return agent - except Exception as e: - self.db.rollback() - logger.error(f"Error creating agent", exc_info=True) - raise - - async def update_agent(self, agent: Agent) -> Agent: - """Update an existing agent.""" - try: - self.db.commit() - self.db.refresh(agent) - logger.info(f"Updated agent: {agent.id}") - return agent - except Exception as e: - self.db.rollback() - logger.error(f"Error updating agent {agent.id}", exc_info=True) - raise - - async def delete_agent(self, agent_id: str) -> bool: - """Delete an agent.""" - try: - agent = await self.get_agent(agent_id) - if agent: - self.db.delete(agent) - self.db.commit() - logger.info(f"Deleted agent: {agent_id}") - return True - return False - except Exception as e: - self.db.rollback() - logger.error(f"Error deleting agent {agent_id}", exc_info=True) - return False - - async def search_agents( - self, - query: str, - filters: Optional[Dict[str, Any]] = None, - limit: int = 10 - ) -> List[Agent]: - """Search agents by name or description.""" - try: - db_query = self.db.query(Agent) - - # Text search - search_filter = or_( - Agent.name.ilike(f"%{query}%"), - Agent.description.ilike(f"%{query}%") - ) - db_query = db_query.filter(search_filter) - - # Apply additional filters - if filters: - for key, value in filters.items(): - if hasattr(Agent, key): - if isinstance(value, list): - db_query = db_query.filter(getattr(Agent, key).in_(value)) - else: - db_query = db_query.filter(getattr(Agent, key) == value) - - return db_query.limit(limit).all() - - except Exception as e: - logger.error(f"Error searching agents with query: {query}", exc_info=True) - return [] - - async def count_agents(self, filters: Optional[Dict[str, Any]] = None) -> int: - """Count agents with optional filtering.""" - try: - query = self.db.query(Agent) - - # Apply filters - if filters: - for key, value in filters.items(): - if hasattr(Agent, key): - if isinstance(value, list): - query = query.filter(getattr(Agent, key).in_(value)) - else: - query = query.filter(getattr(Agent, key) == value) - - return query.count() - - except Exception as e: - logger.error(f"Error counting agents", exc_info=True) - return 0 \ No newline at end of file diff --git a/python/valuecell/server/db/repositories/asset_repository.py b/python/valuecell/server/db/repositories/asset_repository.py deleted file mode 100644 index 7cc12cc4e..000000000 --- a/python/valuecell/server/db/repositories/asset_repository.py +++ /dev/null @@ -1,232 +0,0 @@ -"""Asset repository for ValueCell Server.""" - -from typing import List, Optional, Dict, Any -from datetime import datetime -from sqlalchemy.orm import Session -from sqlalchemy import and_, or_, desc -from ..models.asset import Asset, AssetPrice -from ...config.logging import get_logger - -logger = get_logger(__name__) - - -class AssetRepository: - """Repository for asset data access.""" - - def __init__(self, db: Session): - """Initialize asset repository.""" - self.db = db - - async def get_asset(self, asset_id: str) -> Optional[Asset]: - """Get asset by ID.""" - try: - return self.db.query(Asset).filter(Asset.id == asset_id).first() - except Exception as e: - logger.error(f"Error getting asset {asset_id}", exc_info=True) - return None - - async def get_asset_by_symbol(self, symbol: str) -> Optional[Asset]: - """Get asset by symbol.""" - try: - return self.db.query(Asset).filter(Asset.symbol == symbol.upper()).first() - except Exception as e: - logger.error(f"Error getting asset by symbol {symbol}", exc_info=True) - return None - - async def list_assets( - self, - skip: int = 0, - limit: int = 100, - filters: Optional[Dict[str, Any]] = None, - order_by: str = "created_at", - order_desc: bool = False, - ) -> List[Asset]: - """List assets with optional filtering and pagination.""" - try: - query = self.db.query(Asset) - - # Apply filters - if filters: - for key, value in filters.items(): - if hasattr(Asset, key): - if isinstance(value, list): - query = query.filter(getattr(Asset, key).in_(value)) - else: - query = query.filter(getattr(Asset, key) == value) - - # Apply ordering - if hasattr(Asset, order_by): - order_column = getattr(Asset, order_by) - if order_desc: - query = query.order_by(order_column.desc()) - else: - query = query.order_by(order_column) - - # Apply pagination - return query.offset(skip).limit(limit).all() - - except Exception as e: - logger.error(f"Error listing assets", exc_info=True) - return [] - - async def search_assets( - self, - query: str, - filters: Optional[Dict[str, Any]] = None, - limit: int = 10 - ) -> List[Asset]: - """Search assets by symbol, name, or description.""" - try: - db_query = self.db.query(Asset) - - # Text search - search_filter = or_( - Asset.symbol.ilike(f"%{query}%"), - Asset.name.ilike(f"%{query}%"), - Asset.description.ilike(f"%{query}%") - ) - db_query = db_query.filter(search_filter) - - # Apply additional filters - if filters: - for key, value in filters.items(): - if hasattr(Asset, key): - if isinstance(value, list): - db_query = db_query.filter(getattr(Asset, key).in_(value)) - else: - db_query = db_query.filter(getattr(Asset, key) == value) - - return db_query.limit(limit).all() - - except Exception as e: - logger.error(f"Error searching assets with query: {query}", exc_info=True) - return [] - - async def create_asset(self, asset: Asset) -> Asset: - """Create a new asset.""" - try: - self.db.add(asset) - self.db.commit() - self.db.refresh(asset) - logger.info(f"Created asset: {asset.id}") - return asset - except Exception as e: - self.db.rollback() - logger.error(f"Error creating asset", exc_info=True) - raise - - async def update_asset(self, asset: Asset) -> Asset: - """Update an existing asset.""" - try: - self.db.commit() - self.db.refresh(asset) - logger.info(f"Updated asset: {asset.id}") - return asset - except Exception as e: - self.db.rollback() - logger.error(f"Error updating asset {asset.id}", exc_info=True) - raise - - async def delete_asset(self, asset_id: str) -> bool: - """Delete an asset.""" - try: - asset = await self.get_asset(asset_id) - if asset: - self.db.delete(asset) - self.db.commit() - logger.info(f"Deleted asset: {asset_id}") - return True - return False - except Exception as e: - self.db.rollback() - logger.error(f"Error deleting asset {asset_id}", exc_info=True) - return False - - # Asset Price methods - - async def get_latest_price(self, symbol: str) -> Optional[AssetPrice]: - """Get the latest price for an asset.""" - try: - return ( - self.db.query(AssetPrice) - .filter(AssetPrice.symbol == symbol.upper()) - .order_by(desc(AssetPrice.timestamp)) - .first() - ) - except Exception as e: - logger.error(f"Error getting latest price for {symbol}", exc_info=True) - return None - - async def get_price_history( - self, - symbol: str, - start_date: Optional[datetime] = None, - end_date: Optional[datetime] = None, - limit: int = 100 - ) -> List[AssetPrice]: - """Get price history for an asset.""" - try: - query = ( - self.db.query(AssetPrice) - .filter(AssetPrice.symbol == symbol.upper()) - ) - - if start_date: - query = query.filter(AssetPrice.timestamp >= start_date) - if end_date: - query = query.filter(AssetPrice.timestamp <= end_date) - - return ( - query - .order_by(desc(AssetPrice.timestamp)) - .limit(limit) - .all() - ) - - except Exception as e: - logger.error(f"Error getting price history for {symbol}", exc_info=True) - return [] - - async def create_price(self, price: AssetPrice) -> AssetPrice: - """Create a new price record.""" - try: - self.db.add(price) - self.db.commit() - self.db.refresh(price) - return price - except Exception as e: - self.db.rollback() - logger.error(f"Error creating price record", exc_info=True) - raise - - async def bulk_create_prices(self, prices: List[AssetPrice]) -> bool: - """Bulk create price records.""" - try: - self.db.add_all(prices) - self.db.commit() - logger.info(f"Created {len(prices)} price records") - return True - except Exception as e: - self.db.rollback() - logger.error(f"Error bulk creating price records", exc_info=True) - return False - - async def count_assets(self, filters: Optional[Dict[str, Any]] = None) -> int: - """Count assets with optional filtering.""" - try: - query = self.db.query(Asset) - - # Apply filters - if filters: - for key, value in filters.items(): - if hasattr(Asset, key): - if isinstance(value, list): - query = query.filter(getattr(Asset, key).in_(value)) - else: - query = query.filter(getattr(Asset, key) == value) - - return query.count() - - except Exception as e: - logger.error(f"Error counting assets", exc_info=True) - return 0 \ No newline at end of file From 4f4f2b5aa44aaf54d3706f4a897cf40cf3356653 Mon Sep 17 00:00:00 2001 From: zhonghao lu Date: Mon, 15 Sep 2025 15:29:46 +0800 Subject: [PATCH 04/10] remove useless code --- python/valuecell/server/config/database.py | 40 -------------- python/valuecell/server/config/logging.py | 64 ---------------------- python/valuecell/server/config/settings.py | 1 - python/valuecell/server/db/models/base.py | 1 - python/valuecell/server/requirements.txt | 53 ------------------ 5 files changed, 159 deletions(-) delete mode 100644 python/valuecell/server/config/database.py delete mode 100644 python/valuecell/server/config/logging.py delete mode 100644 python/valuecell/server/requirements.txt diff --git a/python/valuecell/server/config/database.py b/python/valuecell/server/config/database.py deleted file mode 100644 index 94f73beba..000000000 --- a/python/valuecell/server/config/database.py +++ /dev/null @@ -1,40 +0,0 @@ -"""Database configuration for ValueCell Server.""" - -from sqlalchemy import create_engine -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import sessionmaker -from .settings import get_settings - -settings = get_settings() - -# Create database engine -engine = create_engine( - settings.DATABASE_URL, - echo=settings.DB_ECHO, - pool_pre_ping=True, -) - -# Create session factory -SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) - -# Create base class for models -Base = declarative_base() - - -def get_db(): - """Get database session.""" - db = SessionLocal() - try: - yield db - finally: - db.close() - - -def create_tables(): - """Create all tables.""" - Base.metadata.create_all(bind=engine) - - -def drop_tables(): - """Drop all tables.""" - Base.metadata.drop_all(bind=engine) \ No newline at end of file diff --git a/python/valuecell/server/config/logging.py b/python/valuecell/server/config/logging.py deleted file mode 100644 index b1df542c9..000000000 --- a/python/valuecell/server/config/logging.py +++ /dev/null @@ -1,64 +0,0 @@ -"""Logging configuration for ValueCell Server.""" - -import logging -import logging.config -from pathlib import Path -from .settings import get_settings - -settings = get_settings() - - -def setup_logging(): - """Setup logging configuration.""" - log_config = { - "version": 1, - "disable_existing_loggers": False, - "formatters": { - "default": { - "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s", - }, - "json": { - "format": '{"timestamp": "%(asctime)s", "name": "%(name)s", "level": "%(levelname)s", "message": "%(message)s"}', - }, - }, - "handlers": { - "console": { - "class": "logging.StreamHandler", - "level": settings.LOG_LEVEL, - "formatter": settings.LOG_FORMAT if settings.LOG_FORMAT in ["default", "json"] else "default", - "stream": "ext://sys.stdout", - }, - "file": { - "class": "logging.handlers.RotatingFileHandler", - "level": settings.LOG_LEVEL, - "formatter": settings.LOG_FORMAT if settings.LOG_FORMAT in ["default", "json"] else "default", - "filename": settings.LOGS_DIR / "valuecell.log", - "maxBytes": 10485760, # 10MB - "backupCount": 5, - }, - }, - "loggers": { - "": { - "level": settings.LOG_LEVEL, - "handlers": ["console", "file"], - "propagate": False, - }, - "uvicorn": { - "level": "INFO", - "handlers": ["console"], - "propagate": False, - }, - "sqlalchemy": { - "level": "WARNING", - "handlers": ["console"], - "propagate": False, - }, - }, - } - - logging.config.dictConfig(log_config) - - -def get_logger(name: str) -> logging.Logger: - """Get logger instance.""" - return logging.getLogger(name) \ No newline at end of file diff --git a/python/valuecell/server/config/settings.py b/python/valuecell/server/config/settings.py index b8ebbb94c..fb71c8f03 100644 --- a/python/valuecell/server/config/settings.py +++ b/python/valuecell/server/config/settings.py @@ -1,7 +1,6 @@ """Settings configuration for ValueCell Server.""" import os -from typing import List, Optional from pathlib import Path from functools import lru_cache diff --git a/python/valuecell/server/db/models/base.py b/python/valuecell/server/db/models/base.py index 59b61b0c7..0f1b2a96b 100644 --- a/python/valuecell/server/db/models/base.py +++ b/python/valuecell/server/db/models/base.py @@ -1,7 +1,6 @@ """Base model for ValueCell Server.""" from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import DeclarativeBase # Create the base class for all models Base = declarative_base() diff --git a/python/valuecell/server/requirements.txt b/python/valuecell/server/requirements.txt deleted file mode 100644 index a0ed9e8de..000000000 --- a/python/valuecell/server/requirements.txt +++ /dev/null @@ -1,53 +0,0 @@ -# FastAPI and web framework -fastapi==0.104.1 -uvicorn[standard]==0.24.0 -pydantic==2.5.0 -pydantic-settings==2.1.0 - -# Database -sqlalchemy==2.0.23 -alembic==1.13.1 -psycopg2-binary==2.9.9 - -# Redis for caching -redis==5.0.1 -hiredis==2.2.3 - -# HTTP client -httpx==0.25.2 -aiohttp==3.9.1 - -# Authentication and security -python-jose[cryptography]==3.3.0 -passlib[bcrypt]==1.7.4 -python-multipart==0.0.6 - -# Logging and monitoring -structlog==23.2.0 - -# Configuration -python-dotenv==1.0.0 - -# Data processing -pandas==2.1.4 -numpy==1.26.2 - -# Financial data -yfinance==0.2.28 -alpha-vantage==2.3.1 - -# Testing -pytest==7.4.3 -pytest-asyncio==0.21.1 -pytest-cov==4.1.0 -httpx==0.25.2 # for testing FastAPI - -# Development tools -black==23.11.0 -isort==5.12.0 -flake8==6.1.0 -mypy==1.7.1 - -# Utilities -click==8.1.7 -rich==13.7.0 \ No newline at end of file From e8293e710277588ec74e0a4292f46ad893494f89 Mon Sep 17 00:00:00 2001 From: zhonghao lu Date: Mon, 15 Sep 2025 15:46:25 +0800 Subject: [PATCH 05/10] recovery file --- python/valuecell/api/i18n_api.py | 433 ++++++++++++ python/valuecell/services/__init__.py | 14 - python/valuecell/services/agent_context.py | 161 ----- python/valuecell/services/assets/__init__.py | 51 ++ .../services/assets/asset_service.py | 636 ++++++++++++++++++ python/valuecell/services/i18n_service.py | 325 +++++++++ 6 files changed, 1445 insertions(+), 175 deletions(-) create mode 100644 python/valuecell/api/i18n_api.py delete mode 100644 python/valuecell/services/__init__.py delete mode 100644 python/valuecell/services/agent_context.py create mode 100644 python/valuecell/services/assets/__init__.py create mode 100644 python/valuecell/services/assets/asset_service.py create mode 100644 python/valuecell/services/i18n_service.py diff --git a/python/valuecell/api/i18n_api.py b/python/valuecell/api/i18n_api.py new file mode 100644 index 000000000..8212af614 --- /dev/null +++ b/python/valuecell/api/i18n_api.py @@ -0,0 +1,433 @@ +"""Standalone i18n API module for ValueCell.""" + +from typing import Dict, Any, Optional +from fastapi import APIRouter, HTTPException, Header +from datetime import datetime + +from .schemas import ( + SuccessResponse, + LanguageRequest, + TimezoneRequest, + LanguageDetectionRequest, + TranslationRequest, + DateTimeFormatRequest, + NumberFormatRequest, + CurrencyFormatRequest, + UserI18nSettingsRequest, + AgentI18nContext, +) +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 ( + detect_browser_language, + get_common_timezones, + get_timezone_display_name, + validate_language_code, + validate_timezone, +) + + +class I18nAPI: + """Standalone i18n API class.""" + + def __init__(self): + """Initialize i18n API.""" + self.i18n_service = get_i18n_service() + self.settings = get_settings() + + # User context storage (in production, use Redis or database) + self._user_contexts: Dict[str, Dict[str, Any]] = {} + + # Create router + self.router = self._create_router() + + def _create_router(self) -> APIRouter: + """Create FastAPI router for i18n endpoints.""" + 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"] + ) + 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"]) + + # 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"]) + + # 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"] + ) + + # Agent context + router.add_api_route("/agent/context", self.get_agent_context, methods=["GET"]) + + return router + + def _get_user_context(self, user_id: Optional[str]) -> Dict[str, Any]: + """Get user context and apply to i18n service.""" + if user_id and user_id in self._user_contexts: + user_context = self._user_contexts[user_id] + self.i18n_service.set_language(user_context.get("language", "en-US")) + self.i18n_service.set_timezone(user_context.get("timezone", "UTC")) + return user_context + return {"language": "en-US", "timezone": "UTC"} + + 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: + """Get current i18n configuration.""" + self._get_user_context(user_id) + + return SuccessResponse( + message="I18n configuration retrieved successfully", + data=self.i18n_service.to_dict(), + ) + + async def get_supported_languages(self) -> SuccessResponse: + """Get supported languages.""" + languages = [ + { + "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(), + }, + ) + + async def set_language( + self, + request: LanguageRequest, + user_id: Optional[str] = Header(None, alias="X-User-ID"), + ) -> SuccessResponse: + """Set current language.""" + if not validate_language_code(request.language): + raise HTTPException( + status_code=400, + detail=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") + + # Save user context + if user_id: + if user_id not in self._user_contexts: + self._user_contexts[user_id] = {} + self._user_contexts[user_id]["language"] = request.language + self._user_contexts[user_id]["timezone"] = ( + self.i18n_service.get_current_timezone() + ) + + return SuccessResponse( + message="Language updated successfully", + data={ + "language": request.language, + "timezone": self.i18n_service.get_current_timezone(), + }, + ) + + async def get_timezones(self) -> SuccessResponse: + """Get available timezones.""" + common_timezones = get_common_timezones() + timezone_list = [ + { + "value": tz, + "label": get_timezone_display_name(tz), + "is_current": tz == self.i18n_service.get_current_timezone(), + } + for tz in common_timezones + ] + + # Add language-specific timezones if not in common list + for lang_tz in LANGUAGE_TIMEZONE_MAPPING.values(): + if lang_tz not in common_timezones: + timezone_list.append( + { + "value": lang_tz, + "label": get_timezone_display_name(lang_tz), + "is_current": lang_tz + == self.i18n_service.get_current_timezone(), + } + ) + + # Sort by label + timezone_list.sort(key=lambda x: x["label"]) + + return SuccessResponse( + message="Timezones retrieved successfully", + data={ + "timezones": timezone_list, + "current": self.i18n_service.get_current_timezone(), + }, + ) + + async def set_timezone( + self, + request: TimezoneRequest, + user_id: Optional[str] = Header(None, alias="X-User-ID"), + ) -> SuccessResponse: + """Set current timezone.""" + if not validate_timezone(request.timezone): + raise HTTPException( + status_code=400, detail=f"Timezone '{request.timezone}' is not valid" + ) + + success = self.i18n_service.set_timezone(request.timezone) + if not success: + raise HTTPException(status_code=500, detail="Failed to set timezone") + + # Save user context + if user_id: + if user_id not in self._user_contexts: + self._user_contexts[user_id] = {} + self._user_contexts[user_id]["timezone"] = request.timezone + + return SuccessResponse( + message="Timezone updated successfully", + data={ + "timezone": request.timezone, + "display_name": get_timezone_display_name(request.timezone), + }, + ) + + async def detect_language( + self, request: LanguageDetectionRequest + ) -> SuccessResponse: + """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], + }, + ) + + async def translate(self, request: TranslationRequest) -> SuccessResponse: + """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, + }, + ) + except Exception as e: + raise HTTPException( + status_code=500, + detail=f"Failed to translate key '{request.key}': {str(e)}", + ) + + async def format_datetime(self, request: DateTimeFormatRequest) -> SuccessResponse: + """Format datetime.""" + try: + # Parse ISO datetime string + dt = datetime.fromisoformat(request.datetime.replace("Z", "+00:00")) + formatted_dt = self.i18n_service.format_datetime(dt, request.format_type) + + return SuccessResponse( + message="Datetime formatted successfully", + data={ + "original": request.datetime, + "formatted": formatted_dt, + "format_type": request.format_type, + "language": self.i18n_service.get_current_language(), + "timezone": self.i18n_service.get_current_timezone(), + }, + ) + except Exception as e: + raise HTTPException( + status_code=400, detail=f"Failed to format datetime: {str(e)}" + ) + + async def format_number(self, request: NumberFormatRequest) -> SuccessResponse: + """Format number.""" + try: + formatted_number = self.i18n_service.format_number( + request.number, request.decimal_places + ) + + return SuccessResponse( + message="Number formatted successfully", + data={ + "original": request.number, + "formatted": formatted_number, + "decimal_places": request.decimal_places, + "language": self.i18n_service.get_current_language(), + }, + ) + except Exception as e: + raise HTTPException( + status_code=500, detail=f"Failed to format number: {str(e)}" + ) + + async def format_currency(self, request: CurrencyFormatRequest) -> SuccessResponse: + """Format currency.""" + try: + formatted_currency = self.i18n_service.format_currency( + request.amount, request.decimal_places + ) + + return SuccessResponse( + message="Currency formatted successfully", + data={ + "original": request.amount, + "formatted": formatted_currency, + "decimal_places": request.decimal_places, + "language": self.i18n_service.get_current_language(), + "currency_symbol": self.i18n_service._i18n_config.get_currency_symbol(), + }, + ) + except Exception as e: + raise HTTPException( + status_code=500, detail=f"Failed to format currency: {str(e)}" + ) + + async def get_user_settings( + self, user_id: str = Header(..., alias="X-User-ID") + ) -> SuccessResponse: + """Get user's i18n settings.""" + user_context = self._user_contexts.get( + user_id, {"language": "en-US", "timezone": "UTC"} + ) + + return SuccessResponse( + message="User i18n settings retrieved successfully", + data={ + "user_id": user_id, + "language": user_context.get("language", "en-US"), + "timezone": user_context.get("timezone", "UTC"), + }, + ) + + async def update_user_settings( + self, + request: UserI18nSettingsRequest, + user_id: str = Header(..., alias="X-User-ID"), + ) -> SuccessResponse: + """Update user's i18n settings.""" + if user_id not in self._user_contexts: + self._user_contexts[user_id] = {} + + user_context = self._user_contexts[user_id] + + if request.language: + if not validate_language_code(request.language): + raise HTTPException( + status_code=400, + detail=f"Language '{request.language}' is not supported", + ) + user_context["language"] = request.language + self.i18n_service.set_language(request.language) + + if request.timezone: + if not validate_timezone(request.timezone): + raise HTTPException( + status_code=400, + detail=f"Timezone '{request.timezone}' is not valid", + ) + user_context["timezone"] = request.timezone + self.i18n_service.set_timezone(request.timezone) + + return SuccessResponse( + message="User i18n settings updated successfully", + data={ + "user_id": user_id, + "language": user_context.get("language"), + "timezone": user_context.get("timezone"), + }, + ) + + 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: + """Get i18n context for agent communication.""" + # Load user-specific settings + self._get_user_context(user_id) + + context = AgentI18nContext( + language=self.i18n_service.get_current_language(), + timezone=self.i18n_service.get_current_timezone(), + currency_symbol=self.i18n_service._i18n_config.get_currency_symbol(), + date_format=self.i18n_service._i18n_config.get_date_format(), + time_format=self.i18n_service._i18n_config.get_time_format(), + number_format=self.i18n_service._i18n_config.get_number_format(), + user_id=user_id, + session_id=session_id, + ) + + return SuccessResponse( + message="Agent i18n context retrieved successfully", data=context.dict() + ) + + def get_user_context(self, user_id: str) -> Dict[str, Any]: + """Get user context for agents.""" + return self._user_contexts.get( + user_id, {"language": "en-US", "timezone": "UTC"} + ) + + def set_user_context(self, user_id: str, context: Dict[str, Any]): + """Set user context for agents.""" + if user_id not in self._user_contexts: + self._user_contexts[user_id] = {} + self._user_contexts[user_id].update(context) + + +# Global i18n API instance +_i18n_api: Optional[I18nAPI] = None + + +def get_i18n_api() -> I18nAPI: + """Get global i18n API instance.""" + global _i18n_api + if _i18n_api is None: + _i18n_api = I18nAPI() + return _i18n_api + + +def create_i18n_router() -> APIRouter: + """Create i18n router for inclusion in main app.""" + return get_i18n_api().router diff --git a/python/valuecell/services/__init__.py b/python/valuecell/services/__init__.py deleted file mode 100644 index 9b4c6b348..000000000 --- a/python/valuecell/services/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -"""ValueCell Services Module. - -This module provides high-level service layers for various business operations -including agent context management. -""" - -# Agent context service -from .agent_context import AgentContextManager, get_agent_context - -__all__ = [ - # Agent context services - "AgentContextManager", - "get_agent_context", -] diff --git a/python/valuecell/services/agent_context.py b/python/valuecell/services/agent_context.py deleted file mode 100644 index 94bd82530..000000000 --- a/python/valuecell/services/agent_context.py +++ /dev/null @@ -1,161 +0,0 @@ -"""Agent context management for ValueCell application.""" - -from typing import Optional -from datetime import datetime -import threading -from contextlib import contextmanager - -from ..api.schemas import AgentI18nContext - - -class AgentContextManager: - """Manages context for agents to access user i18n settings.""" - - def __init__(self): - """Initialize agent context manager.""" - self._local = threading.local() - - def set_user_context(self, user_id: str, session_id: Optional[str] = None): - """Set current user context for the agent.""" - # Store in thread local storage with default values - self._local.user_id = user_id - self._local.session_id = session_id - self._local.language = "en-US" # Default language - self._local.timezone = "UTC" # Default timezone - - def get_current_user_id(self) -> Optional[str]: - """Get current user ID.""" - return getattr(self._local, "user_id", None) - - def get_current_session_id(self) -> Optional[str]: - """Get current session ID.""" - return getattr(self._local, "session_id", None) - - def get_current_language(self) -> str: - """Get current user's language.""" - return getattr(self._local, "language", "en-US") - - def get_current_timezone(self) -> str: - """Get current user's timezone.""" - return getattr(self._local, "timezone", "UTC") - - def get_i18n_context(self) -> AgentI18nContext: - """Get complete i18n context for agent.""" - return AgentI18nContext( - language=self.get_current_language(), - timezone=self.get_current_timezone(), - currency_symbol="$", # Default currency symbol - date_format="YYYY-MM-DD", # Default date format - time_format="HH:mm:ss", # Default time format - number_format="en-US", # Default number format - user_id=self.get_current_user_id(), - session_id=self.get_current_session_id(), - ) - - def translate(self, key: str, **variables) -> str: - """Translate using current user's language.""" - # i18n service removed, return key as fallback - return key - - def format_datetime(self, dt: datetime, format_type: str = "datetime") -> str: - """Format datetime using current user's settings.""" - # i18n service removed, return basic format - return dt.isoformat() - - def format_number(self, number: float, decimal_places: int = 2) -> str: - """Format number using current user's settings.""" - # i18n service removed, return basic format - return f"{number:.{decimal_places}f}" - - def format_currency(self, amount: float, decimal_places: int = 2) -> str: - """Format currency using current user's settings.""" - # i18n service removed, return basic format - return f"${amount:.{decimal_places}f}" - - @contextmanager - def user_context(self, user_id: str, session_id: Optional[str] = None): - """Context manager for temporary user context.""" - # Save current context - old_user_id = getattr(self._local, "user_id", None) - old_session_id = getattr(self._local, "session_id", None) - old_language = getattr(self._local, "language", "en-US") - old_timezone = getattr(self._local, "timezone", "UTC") - - try: - # Set new context - self.set_user_context(user_id, session_id) - yield self - finally: - # Restore old context - if old_user_id: - self._local.user_id = old_user_id - self._local.session_id = old_session_id - self._local.language = old_language - self._local.timezone = old_timezone - self.i18n_service.set_language(old_language) - self.i18n_service.set_timezone(old_timezone) - else: - # Clear context - if hasattr(self._local, "user_id"): - delattr(self._local, "user_id") - if hasattr(self._local, "session_id"): - delattr(self._local, "session_id") - if hasattr(self._local, "language"): - delattr(self._local, "language") - if hasattr(self._local, "timezone"): - delattr(self._local, "timezone") - - def clear_context(self): - """Clear current user context.""" - if hasattr(self._local, "user_id"): - delattr(self._local, "user_id") - if hasattr(self._local, "session_id"): - delattr(self._local, "session_id") - if hasattr(self._local, "language"): - delattr(self._local, "language") - if hasattr(self._local, "timezone"): - delattr(self._local, "timezone") - - -# Global agent context manager -_agent_context: Optional[AgentContextManager] = None - - -def get_agent_context() -> AgentContextManager: - """Get global agent context manager.""" - global _agent_context - if _agent_context is None: - _agent_context = AgentContextManager() - return _agent_context - - -def reset_agent_context(): - """Reset global agent context manager.""" - global _agent_context - _agent_context = None - - -# Convenience functions for agents -def set_user_context(user_id: str, session_id: Optional[str] = None): - """Set user context for current agent (convenience function).""" - return get_agent_context().set_user_context(user_id, session_id) - - -def get_current_user_id() -> Optional[str]: - """Get current user ID (convenience function).""" - return get_agent_context().get_current_user_id() - - -def get_i18n_context() -> AgentI18nContext: - """Get i18n context (convenience function).""" - return get_agent_context().get_i18n_context() - - -def t(key: str, **variables) -> str: - """Translate using current user context (convenience function).""" - return get_agent_context().translate(key, **variables) - - -def user_context(user_id: str, session_id: Optional[str] = None): - """Context manager for user context (convenience function).""" - return get_agent_context().user_context(user_id, session_id) diff --git a/python/valuecell/services/assets/__init__.py b/python/valuecell/services/assets/__init__.py new file mode 100644 index 000000000..18873f983 --- /dev/null +++ b/python/valuecell/services/assets/__init__.py @@ -0,0 +1,51 @@ +"""ValueCell Asset Service Module. + +This module provides high-level asset service functionality for financial asset management, +search, price retrieval, and watchlist operations with internationalization support. + +Key Features: +- Asset search with localization +- Real-time and historical price data +- Watchlist management +- Multi-language support +- Integration with multiple data adapters + +Usage Example: + ```python + from valuecell.services.assets import ( + get_asset_service, search_assets, add_to_watchlist + ) + + # Search for assets + results = search_assets("AAPL", language="zh-Hans") + + # Add to watchlist + add_to_watchlist(user_id="user123", ticker="NASDAQ:AAPL") + ``` +""" + +from .asset_service import ( + AssetService, + get_asset_service, + reset_asset_service, + search_assets, + get_asset_info, + get_asset_price, + add_to_watchlist, + get_watchlist, +) + +__version__ = "1.0.0" + +__all__ = [ + # Service class + "AssetService", + "get_asset_service", + "reset_asset_service", + # Convenience functions + "search_assets", + "get_asset_info", + "get_asset_price", + "add_to_watchlist", + "get_watchlist", +] diff --git a/python/valuecell/services/assets/asset_service.py b/python/valuecell/services/assets/asset_service.py new file mode 100644 index 000000000..1e3093f1c --- /dev/null +++ b/python/valuecell/services/assets/asset_service.py @@ -0,0 +1,636 @@ +"""Asset service for asset management and watchlist operations. + +This module provides high-level service functions for asset search, watchlist management, +and price data retrieval with i18n support. +""" + +import logging +from typing import Dict, List, Optional, Any +from datetime import datetime + +from ...adapters.assets.manager import get_adapter_manager, get_watchlist_manager +from ...adapters.assets.i18n_integration import get_asset_i18n_service +from ...adapters.assets.types import AssetSearchQuery, AssetType +from ...config.i18n import get_i18n_config + +logger = logging.getLogger(__name__) + + +class AssetService: + """High-level service for asset operations with i18n support.""" + + def __init__(self): + """Initialize asset service.""" + self.adapter_manager = get_adapter_manager() + self.watchlist_manager = get_watchlist_manager() + self.i18n_service = get_asset_i18n_service() + + def search_assets( + self, + query: str, + asset_types: Optional[List[str]] = None, + exchanges: Optional[List[str]] = None, + countries: Optional[List[str]] = None, + limit: int = 50, + language: Optional[str] = None, + ) -> Dict[str, Any]: + """Search for assets with localization support. + + Args: + query: Search query string + asset_types: Filter by asset types (optional) + exchanges: Filter by exchanges (optional) + countries: Filter by countries (optional) + limit: Maximum number of results + language: Language for localized results + + Returns: + Dictionary containing search results and metadata + """ + try: + # Convert string asset types to enum + parsed_asset_types = None + if asset_types: + parsed_asset_types = [] + for asset_type_str in asset_types: + try: + parsed_asset_types.append(AssetType(asset_type_str.lower())) + except ValueError: + logger.warning(f"Invalid asset type: {asset_type_str}") + + # Create search query + search_query = AssetSearchQuery( + query=query, + asset_types=parsed_asset_types, + exchanges=exchanges, + countries=countries, + limit=limit, + language=language or get_i18n_config().language, + ) + + # Perform search + results = self.adapter_manager.search_assets(search_query) + + # Localize results + localized_results = self.i18n_service.localize_search_results( + results, language + ) + + # Convert to dictionary format + result_dicts = [] + for result in localized_results: + result_dict = { + "ticker": result.ticker, + "asset_type": result.asset_type.value, + "asset_type_display": self.i18n_service.get_asset_type_display_name( + result.asset_type, language + ), + "names": result.names, + "display_name": result.get_display_name( + language or get_i18n_config().language + ), + "exchange": result.exchange, + "country": result.country, + "currency": result.currency, + "market_status": result.market_status.value, + "market_status_display": self.i18n_service.get_market_status_display_name( + result.market_status, language + ), + "relevance_score": result.relevance_score, + } + result_dicts.append(result_dict) + + return { + "success": True, + "results": result_dicts, + "count": len(result_dicts), + "query": query, + "filters": { + "asset_types": asset_types, + "exchanges": exchanges, + "countries": countries, + "limit": limit, + }, + "language": language or get_i18n_config().language, + } + + except Exception as e: + logger.error(f"Error searching assets: {e}") + return {"success": False, "error": str(e), "results": [], "count": 0} + + def get_asset_info( + self, ticker: str, language: Optional[str] = None + ) -> Dict[str, Any]: + """Get detailed asset information with localization. + + Args: + ticker: Asset ticker in internal format + language: Language for localized content + + Returns: + Dictionary containing asset information + """ + try: + asset = self.adapter_manager.get_asset_info(ticker) + + if not asset: + return {"success": False, "error": "Asset not found", "ticker": ticker} + + # Localize asset + localized_asset = self.i18n_service.localize_asset(asset, language) + + # Convert to dictionary + asset_dict = { + "success": True, + "ticker": localized_asset.ticker, + "asset_type": localized_asset.asset_type.value, + "asset_type_display": self.i18n_service.get_asset_type_display_name( + localized_asset.asset_type, language + ), + "names": localized_asset.names.names, + "display_name": localized_asset.get_localized_name( + language or get_i18n_config().language + ), + "descriptions": localized_asset.descriptions, + "market_info": { + "exchange": localized_asset.market_info.exchange, + "country": localized_asset.market_info.country, + "currency": localized_asset.market_info.currency, + "timezone": localized_asset.market_info.timezone, + "trading_hours": localized_asset.market_info.trading_hours, + "market_status": localized_asset.market_info.market_status.value, + }, + "source_mappings": { + k.value: v for k, v in localized_asset.source_mappings.items() + }, + "properties": localized_asset.properties, + "created_at": localized_asset.created_at.isoformat(), + "updated_at": localized_asset.updated_at.isoformat(), + "is_active": localized_asset.is_active, + } + + return asset_dict + + except Exception as e: + logger.error(f"Error getting asset info for {ticker}: {e}") + return {"success": False, "error": str(e), "ticker": ticker} + + def get_asset_price( + self, ticker: str, language: Optional[str] = None + ) -> Dict[str, Any]: + """Get current price for an asset with localized formatting. + + Args: + ticker: Asset ticker in internal format + language: Language for localized formatting + + Returns: + Dictionary containing price information + """ + try: + price_data = self.adapter_manager.get_real_time_price(ticker) + + if not price_data: + return { + "success": False, + "error": "Price data not available", + "ticker": ticker, + } + + # Format price data with localization + formatted_price = { + "success": True, + "ticker": price_data.ticker, + "price": float(price_data.price), + "price_formatted": self.i18n_service.format_currency_amount( + float(price_data.price), price_data.currency, language + ), + "currency": price_data.currency, + "timestamp": price_data.timestamp.isoformat(), + "volume": float(price_data.volume) if price_data.volume else None, + "open_price": float(price_data.open_price) + if price_data.open_price + else None, + "high_price": float(price_data.high_price) + if price_data.high_price + else None, + "low_price": float(price_data.low_price) + if price_data.low_price + else None, + "close_price": float(price_data.close_price) + if price_data.close_price + else None, + "change": float(price_data.change) if price_data.change else None, + "change_percent": float(price_data.change_percent) + if price_data.change_percent + else None, + "change_percent_formatted": self.i18n_service.format_percentage_change( + float(price_data.change_percent), language + ) + if price_data.change_percent + else None, + "market_cap": float(price_data.market_cap) + if price_data.market_cap + else None, + "market_cap_formatted": self.i18n_service.format_market_cap( + float(price_data.market_cap), price_data.currency, language + ) + if price_data.market_cap + else None, + "source": price_data.source.value if price_data.source else None, + } + + return formatted_price + + except Exception as e: + logger.error(f"Error getting price for {ticker}: {e}") + return {"success": False, "error": str(e), "ticker": ticker} + + def get_multiple_prices( + self, tickers: List[str], language: Optional[str] = None + ) -> Dict[str, Any]: + """Get prices for multiple assets efficiently. + + Args: + tickers: List of asset tickers + language: Language for localized formatting + + Returns: + Dictionary containing price data for all tickers + """ + try: + price_data = self.adapter_manager.get_multiple_prices(tickers) + + formatted_prices = {} + + for ticker, price in price_data.items(): + if price: + formatted_prices[ticker] = { + "price": float(price.price), + "price_formatted": self.i18n_service.format_currency_amount( + float(price.price), price.currency, language + ), + "currency": price.currency, + "timestamp": price.timestamp.isoformat(), + "change": float(price.change) if price.change else None, + "change_percent": float(price.change_percent) + if price.change_percent + else None, + "change_percent_formatted": self.i18n_service.format_percentage_change( + float(price.change_percent), language + ) + if price.change_percent + else None, + "volume": float(price.volume) if price.volume else None, + "market_cap": float(price.market_cap) + if price.market_cap + else None, + "market_cap_formatted": self.i18n_service.format_market_cap( + float(price.market_cap), price.currency, language + ) + if price.market_cap + else None, + "source": price.source.value if price.source else None, + } + else: + formatted_prices[ticker] = None + + return { + "success": True, + "prices": formatted_prices, + "count": len([p for p in formatted_prices.values() if p is not None]), + "requested_count": len(tickers), + } + + except Exception as e: + logger.error(f"Error getting multiple prices: {e}") + return {"success": False, "error": str(e), "prices": {}} + + def create_watchlist( + self, + user_id: str, + name: str = "My Watchlist", + description: str = "", + is_default: bool = False, + ) -> Dict[str, Any]: + """Create a new watchlist for a user. + + Args: + user_id: User identifier + name: Watchlist name + description: Watchlist description + is_default: Whether this is the default watchlist + + Returns: + Dictionary containing created watchlist information + """ + try: + watchlist = self.watchlist_manager.create_watchlist( + user_id, name, description, is_default + ) + + return { + "success": True, + "watchlist": { + "user_id": watchlist.user_id, + "name": watchlist.name, + "description": watchlist.description, + "created_at": watchlist.created_at.isoformat(), + "updated_at": watchlist.updated_at.isoformat(), + "is_default": watchlist.is_default, + "is_public": watchlist.is_public, + "items_count": len(watchlist.items), + }, + } + + except Exception as e: + logger.error(f"Error creating watchlist: {e}") + return {"success": False, "error": str(e)} + + def add_to_watchlist( + self, + user_id: str, + ticker: str, + watchlist_name: Optional[str] = None, + notes: str = "", + ) -> Dict[str, Any]: + """Add an asset to a watchlist. + + Args: + user_id: User identifier + ticker: Asset ticker to add + watchlist_name: Watchlist name (uses default if None) + notes: User notes about the asset + + Returns: + Dictionary containing operation result + """ + try: + success = self.watchlist_manager.add_asset_to_watchlist( + user_id, ticker, watchlist_name, notes + ) + + if success: + return { + "success": True, + "message": "Asset added to watchlist successfully", + "ticker": ticker, + "user_id": user_id, + "watchlist_name": watchlist_name, + } + else: + return { + "success": False, + "error": "Failed to add asset to watchlist", + "ticker": ticker, + } + + except Exception as e: + logger.error(f"Error adding {ticker} to watchlist: {e}") + return {"success": False, "error": str(e), "ticker": ticker} + + def remove_from_watchlist( + self, user_id: str, ticker: str, watchlist_name: Optional[str] = None + ) -> Dict[str, Any]: + """Remove an asset from a watchlist. + + Args: + user_id: User identifier + ticker: Asset ticker to remove + watchlist_name: Watchlist name (uses default if None) + + Returns: + Dictionary containing operation result + """ + try: + success = self.watchlist_manager.remove_asset_from_watchlist( + user_id, ticker, watchlist_name + ) + + if success: + return { + "success": True, + "message": "Asset removed from watchlist successfully", + "ticker": ticker, + "user_id": user_id, + "watchlist_name": watchlist_name, + } + else: + return { + "success": False, + "error": "Asset not found in watchlist or watchlist not found", + "ticker": ticker, + } + + except Exception as e: + logger.error(f"Error removing {ticker} from watchlist: {e}") + return {"success": False, "error": str(e), "ticker": ticker} + + def get_watchlist( + self, + user_id: str, + watchlist_name: Optional[str] = None, + include_prices: bool = True, + language: Optional[str] = None, + ) -> Dict[str, Any]: + """Get watchlist with asset information and prices. + + Args: + user_id: User identifier + watchlist_name: Watchlist name (uses default if None) + include_prices: Whether to include current prices + language: Language for localized content + + Returns: + Dictionary containing watchlist data + """ + try: + # Get watchlist + if watchlist_name: + watchlist = self.watchlist_manager.get_watchlist( + user_id, watchlist_name + ) + else: + watchlist = self.watchlist_manager.get_default_watchlist(user_id) + + if not watchlist: + return { + "success": False, + "error": "Watchlist not found", + "user_id": user_id, + "watchlist_name": watchlist_name, + } + + # Get asset information and prices + assets_data = [] + tickers = watchlist.get_tickers() + + # Get prices if requested + prices_data = {} + if include_prices and tickers: + prices_result = self.get_multiple_prices(tickers, language) + if prices_result["success"]: + prices_data = prices_result["prices"] + + # Build asset data + for item in sorted(watchlist.items, key=lambda x: x.order): + asset_data = { + "ticker": item.ticker, + "display_name": self.i18n_service.get_localized_asset_name( + item.ticker, language + ), + "added_at": item.added_at.isoformat(), + "order": item.order, + "notes": item.notes, + "alerts": item.alerts, + } + + # Add price data if available + if item.ticker in prices_data and prices_data[item.ticker]: + asset_data["price_data"] = prices_data[item.ticker] + + assets_data.append(asset_data) + + return { + "success": True, + "watchlist": { + "user_id": watchlist.user_id, + "name": watchlist.name, + "description": watchlist.description, + "created_at": watchlist.created_at.isoformat(), + "updated_at": watchlist.updated_at.isoformat(), + "is_default": watchlist.is_default, + "is_public": watchlist.is_public, + "items_count": len(watchlist.items), + "assets": assets_data, + }, + } + + except Exception as e: + logger.error(f"Error getting watchlist: {e}") + return {"success": False, "error": str(e), "user_id": user_id} + + def get_user_watchlists(self, user_id: str) -> Dict[str, Any]: + """Get all watchlists for a user. + + Args: + user_id: User identifier + + Returns: + Dictionary containing all user watchlists + """ + try: + watchlists = self.watchlist_manager.get_user_watchlists(user_id) + + watchlists_data = [] + for watchlist in watchlists: + watchlist_data = { + "name": watchlist.name, + "description": watchlist.description, + "created_at": watchlist.created_at.isoformat(), + "updated_at": watchlist.updated_at.isoformat(), + "is_default": watchlist.is_default, + "is_public": watchlist.is_public, + "items_count": len(watchlist.items), + } + watchlists_data.append(watchlist_data) + + return { + "success": True, + "user_id": user_id, + "watchlists": watchlists_data, + "count": len(watchlists_data), + } + + except Exception as e: + logger.error(f"Error getting user watchlists: {e}") + return {"success": False, "error": str(e), "user_id": user_id} + + def get_system_health(self) -> Dict[str, Any]: + """Get system health status for all data adapters. + + Returns: + Dictionary containing health status for all adapters + """ + try: + health_data = self.adapter_manager.health_check() + + # Convert enum keys to strings + health_status = {} + for source, status in health_data.items(): + health_status[source.value] = status + + # Calculate overall health + healthy_count = sum( + 1 + for status in health_status.values() + if status.get("status") == "healthy" + ) + total_count = len(health_status) + + # Determine overall status + if total_count == 0: + overall_status = "no_adapters" + elif healthy_count == total_count: + overall_status = "healthy" + elif healthy_count > 0: + overall_status = "degraded" + else: + overall_status = "unhealthy" + + return { + "success": True, + "overall_status": overall_status, + "healthy_adapters": healthy_count, + "total_adapters": total_count, + "adapters": health_status, + "timestamp": datetime.utcnow().isoformat(), + } + + except Exception as e: + logger.error(f"Error getting system health: {e}") + return {"success": False, "error": str(e), "overall_status": "error"} + + +# Global service instance +_asset_service: Optional[AssetService] = None + + +def get_asset_service() -> AssetService: + """Get global asset service instance.""" + global _asset_service + if _asset_service is None: + _asset_service = AssetService() + return _asset_service + + +def reset_asset_service() -> None: + """Reset global asset service instance (mainly for testing).""" + global _asset_service + _asset_service = None + + +# Convenience functions for direct service access +def search_assets(query: str, **kwargs) -> Dict[str, Any]: + """Convenience function for asset search.""" + return get_asset_service().search_assets(query, **kwargs) + + +def get_asset_info(ticker: str, **kwargs) -> Dict[str, Any]: + """Convenience function for getting asset info.""" + return get_asset_service().get_asset_info(ticker, **kwargs) + + +def get_asset_price(ticker: str, **kwargs) -> Dict[str, Any]: + """Convenience function for getting asset price.""" + return get_asset_service().get_asset_price(ticker, **kwargs) + + +def add_to_watchlist(user_id: str, ticker: str, **kwargs) -> Dict[str, Any]: + """Convenience function for adding to watchlist.""" + return get_asset_service().add_to_watchlist(user_id, ticker, **kwargs) + + +def get_watchlist(user_id: str, **kwargs) -> Dict[str, Any]: + """Convenience function for getting watchlist.""" + return get_asset_service().get_watchlist(user_id, **kwargs) diff --git a/python/valuecell/services/i18n_service.py b/python/valuecell/services/i18n_service.py new file mode 100644 index 000000000..e3cdd2cff --- /dev/null +++ b/python/valuecell/services/i18n_service.py @@ -0,0 +1,325 @@ +"""Internationalization service for ValueCell application.""" + +import json +from pathlib import Path +from typing import Dict, Any, Optional, List +from datetime import datetime + +from ..config.settings import get_settings +from ..config.i18n import get_i18n_config +from ..core.constants import SUPPORTED_LANGUAGE_CODES, DEFAULT_LANGUAGE + + +class TranslationManager: + """Manages translation loading and caching.""" + + def __init__(self, locale_dir: Optional[Path] = None): + """Initialize translation manager. + + Args: + locale_dir: Directory containing translation files + """ + self._locale_dir = locale_dir or get_settings().LOCALE_DIR + self._translations: Dict[str, Dict[str, Any]] = {} + self._load_all_translations() + + def _load_all_translations(self) -> None: + """Load all translation files.""" + for lang_code in SUPPORTED_LANGUAGE_CODES: + self._load_translation(lang_code) + + def _load_translation(self, language: str) -> None: + """Load translation for specific language. + + Args: + language: Language code to load + """ + translation_file = self._locale_dir / f"{language}.json" + + if translation_file.exists(): + try: + with open(translation_file, "r", encoding="utf-8") as f: + self._translations[language] = json.load(f) + except (json.JSONDecodeError, IOError) as e: + print(f"Error loading translation file {translation_file}: {e}") + self._translations[language] = {} + else: + # Create empty translation if file doesn't exist + self._translations[language] = {} + + def get_translation(self, language: str, key: str, **kwargs) -> str: + """Get translated string for given key and language. + + Args: + language: Language code + key: Translation key (supports dot notation for nested keys) + **kwargs: Variables for string formatting + + Returns: + Translated string or key if translation not found + """ + if language not in self._translations: + language = DEFAULT_LANGUAGE + + translations = self._translations.get(language, {}) + + # Support dot notation for nested keys + keys = key.split(".") + value = translations + + try: + for k in keys: + value = value[k] + except (KeyError, TypeError): + # Fallback to default language + if language != DEFAULT_LANGUAGE: + return self.get_translation(DEFAULT_LANGUAGE, key, **kwargs) + return key # Return key if no translation found + + # Format string with provided variables + if isinstance(value, str) and kwargs: + try: + return value.format(**kwargs) + except (KeyError, ValueError): + return value + + return str(value) + + def reload_translations(self) -> None: + """Reload all translation files.""" + self._translations.clear() + self._load_all_translations() + + def get_available_keys(self, language: str) -> List[str]: + """Get all available translation keys for a language. + + Args: + language: Language code + + Returns: + List of available translation keys + """ + translations = self._translations.get(language, {}) + + def _get_keys(obj: Dict[str, Any], prefix: str = "") -> List[str]: + keys = [] + for key, value in obj.items(): + full_key = f"{prefix}.{key}" if prefix else key + if isinstance(value, dict): + keys.extend(_get_keys(value, full_key)) + else: + keys.append(full_key) + return keys + + return _get_keys(translations) + + +class I18nService: + """Main internationalization service.""" + + def __init__(self): + """Initialize i18n service.""" + self._translation_manager = TranslationManager() + self._i18n_config = get_i18n_config() + + def translate(self, key: str, language: Optional[str] = None, **kwargs) -> str: + """Translate a key to current or specified language. + + Args: + key: Translation key + language: Target language (uses current if not specified) + **kwargs: Variables for string formatting + + Returns: + Translated string + """ + target_language = language or self._i18n_config.language + return self._translation_manager.get_translation(target_language, key, **kwargs) + + def t(self, key: str, **kwargs) -> str: + """Short alias for translate method. + + Args: + key: Translation key + **kwargs: Variables for string formatting + + Returns: + Translated string + """ + return self.translate(key, **kwargs) + + def get_current_language(self) -> str: + """Get current language code.""" + return self._i18n_config.language + + def get_current_timezone(self) -> str: + """Get current timezone.""" + return self._i18n_config.timezone + + def set_language(self, language: str) -> bool: + """Set current language. + + Args: + language: Language code to set + + Returns: + True if language was set successfully + """ + if language in SUPPORTED_LANGUAGE_CODES: + self._i18n_config.set_language(language) + get_settings().update_language(language) + return True + return False + + def set_timezone(self, timezone: str) -> bool: + """Set current timezone. + + Args: + timezone: Timezone to set + + Returns: + True if timezone was set successfully + """ + try: + self._i18n_config.set_timezone(timezone) + get_settings().update_timezone(timezone) + return True + except Exception: + return False + + def format_datetime(self, dt: datetime, format_type: str = "datetime") -> str: + """Format datetime according to current language settings. + + Args: + dt: Datetime to format + format_type: Type of format ('date', 'time', 'datetime') + + Returns: + Formatted datetime string + """ + return self._i18n_config.format_datetime(dt, format_type) + + def format_number(self, number: float, decimal_places: int = 2) -> str: + """Format number according to current language settings. + + Args: + number: Number to format + decimal_places: Number of decimal places + + Returns: + Formatted number string + """ + return self._i18n_config.format_number(number, decimal_places) + + def format_currency(self, amount: float, decimal_places: int = 2) -> str: + """Format currency according to current language settings. + + Args: + amount: Amount to format + decimal_places: Number of decimal places + + Returns: + Formatted currency string + """ + return self._i18n_config.format_currency(amount, decimal_places) + + def get_supported_languages(self) -> List[tuple]: + """Get list of supported languages. + + Returns: + List of (code, name) tuples + """ + from ..core.constants import SUPPORTED_LANGUAGES + + return SUPPORTED_LANGUAGES + + def get_language_name(self, language_code: str) -> str: + """Get display name for language code. + + Args: + language_code: Language code + + Returns: + Display name or code if not found + """ + from ..core.constants import SUPPORTED_LANGUAGES + + for code, name in SUPPORTED_LANGUAGES: + if code == language_code: + return name + return language_code + + def reload_translations(self) -> None: + """Reload all translation files.""" + self._translation_manager.reload_translations() + + def get_translation_keys(self, language: Optional[str] = None) -> List[str]: + """Get all available translation keys for a language. + + Args: + language: Language code (uses current if not specified) + + Returns: + List of available translation keys + """ + target_language = language or self._i18n_config.language + return self._translation_manager.get_available_keys(target_language) + + def to_dict(self) -> Dict[str, Any]: + """Get current i18n configuration as dictionary. + + Returns: + Dictionary with i18n configuration + """ + 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(), + } + + +# Global i18n service instance +_i18n_service: Optional[I18nService] = None + + +def get_i18n_service() -> I18nService: + """Get global i18n service instance.""" + global _i18n_service + if _i18n_service is None: + _i18n_service = I18nService() + return _i18n_service + + +def reset_i18n_service() -> None: + """Reset global i18n service instance.""" + global _i18n_service + _i18n_service = None + + +# Convenience functions +def t(key: str, **kwargs) -> str: + """Translate a key (convenience function). + + Args: + key: Translation key + **kwargs: Variables for string formatting + + Returns: + Translated string + """ + return get_i18n_service().translate(key, **kwargs) + + +def translate(key: str, language: Optional[str] = None, **kwargs) -> str: + """Translate a key to specified language (convenience function). + + Args: + key: Translation key + language: Target language + **kwargs: Variables for string formatting + + Returns: + Translated string + """ + return get_i18n_service().translate(key, language, **kwargs) From 63be4c59940ca06ab283de1f2ef273c4144387fc Mon Sep 17 00:00:00 2001 From: zhonghao lu Date: Mon, 15 Sep 2025 15:52:10 +0800 Subject: [PATCH 06/10] recovery file --- .../examples/asset_adapter_example.py | 361 +++++++++++++++ python/valuecell/examples/i18n_example.py | 165 +++++++ python/valuecell/services/__init__.py | 23 + python/valuecell/services/agent_context.py | 169 +++++++ python/valuecell/utils/i18n_utils.py | 426 ++++++++++++++++++ 5 files changed, 1144 insertions(+) create mode 100644 python/valuecell/examples/asset_adapter_example.py create mode 100644 python/valuecell/examples/i18n_example.py create mode 100644 python/valuecell/services/__init__.py create mode 100644 python/valuecell/services/agent_context.py create mode 100644 python/valuecell/utils/i18n_utils.py diff --git a/python/valuecell/examples/asset_adapter_example.py b/python/valuecell/examples/asset_adapter_example.py new file mode 100644 index 000000000..3b82c0c5d --- /dev/null +++ b/python/valuecell/examples/asset_adapter_example.py @@ -0,0 +1,361 @@ +"""Example usage of the ValueCell Asset Data Adapter system. + +This example demonstrates how to configure and use the asset data adapters +for financial data retrieval, search, and watchlist management with i18n support. +""" + +import logging + +from valuecell.adapters.assets import get_adapter_manager +from valuecell.services.assets import ( + get_asset_service, + search_assets, + get_asset_info, + get_asset_price, + add_to_watchlist, + get_watchlist, +) +from valuecell.i18n import set_i18n_config, I18nConfig + +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +def setup_adapters(): + """Configure and initialize data adapters.""" + logger.info("Setting up data adapters...") + + # Get adapter manager + manager = get_adapter_manager() + + # Configure Yahoo Finance (free, no API key required) + try: + manager.configure_yfinance() + logger.info("✓ Yahoo Finance adapter configured") + except Exception as e: + logger.warning(f"✗ Yahoo Finance adapter failed: {e}") + + # Configure TuShare (requires API key) + try: + # Replace with your actual TuShare API key + tushare_api_key = "your_tushare_api_key_here" + if tushare_api_key != "your_tushare_api_key_here": + manager.configure_tushare(api_key=tushare_api_key) + logger.info("✓ TuShare adapter configured") + else: + logger.warning("✗ TuShare API key not provided, skipping") + except Exception as e: + logger.warning(f"✗ TuShare adapter failed: {e}") + + # Configure CoinMarketCap (requires API key for crypto data) + try: + # Replace with your actual CoinMarketCap API key + cmc_api_key = "your_coinmarketcap_api_key_here" + if cmc_api_key != "your_coinmarketcap_api_key_here": + manager.configure_coinmarketcap(api_key=cmc_api_key) + logger.info("✓ CoinMarketCap adapter configured") + else: + logger.warning("✗ CoinMarketCap API key not provided, skipping") + except Exception as e: + logger.warning(f"✗ CoinMarketCap adapter failed: {e}") + + # Configure AKShare (free, no API key required) + # Now supports A-shares, US stocks, Hong Kong stocks, and cryptocurrencies + try: + manager.configure_akshare() + logger.info( + "✓ AKShare adapter configured (supports A-shares, HK stocks, US stocks, and crypto)" + ) + except Exception as e: + logger.warning(f"✗ AKShare adapter failed: {e}") + + # Configure Finnhub (requires API key) + try: + # Replace with your actual Finnhub API key + finnhub_api_key = "your_finnhub_api_key_here" + if finnhub_api_key != "your_finnhub_api_key_here": + manager.configure_finnhub(api_key=finnhub_api_key) + logger.info("✓ Finnhub adapter configured") + else: + logger.warning("✗ Finnhub API key not provided, skipping") + except Exception as e: + logger.warning(f"✗ Finnhub adapter failed: {e}") + + # Check system health + service = get_asset_service() + health = service.get_system_health() + logger.info( + f"System health: {health['overall_status']} " + f"({health['healthy_adapters']}/{health['total_adapters']} adapters)" + ) + + return manager + + +def demonstrate_asset_search(): + """Demonstrate asset search functionality with i18n.""" + logger.info("\n=== Asset Search Demo ===") + + # Search in English + logger.info("Searching for 'AAPL' in English...") + results_en = search_assets("AAPL", language="en-US", limit=5) + + if results_en["success"]: + logger.info(f"Found {results_en['count']} results:") + for result in results_en["results"]: + logger.info( + f" - {result['ticker']}: {result['display_name']} " + f"({result['asset_type_display']})" + ) + + # Search in Chinese + logger.info("\nSearching for '00700.HK' in Chinese...") + results_zh = search_assets("00700.HK", language="zh-Hans", limit=5) + + if results_zh["success"]: + logger.info(f"找到 {results_zh['count']} 个结果:") + for result in results_zh["results"]: + logger.info( + f" - {result['ticker']}: {result['display_name']} " + f"({result['asset_type_display']})" + ) + + # Search for Chinese stocks + logger.info("\nSearching for Chinese stocks...") + results_cn = search_assets("600519", asset_types=["stock"], limit=3) + + if results_cn["success"]: + logger.info(f"Found {results_cn['count']} Chinese stocks:") + for result in results_cn["results"]: + logger.info(f" - {result['ticker']}: {result['display_name']}") + + # Search for cryptocurrencies + logger.info("\nSearching for cryptocurrencies...") + results_crypto = search_assets("BTC-USD", asset_types=["crypto"], limit=3) + + if results_crypto["success"]: + logger.info(f"Found {results_crypto['count']} cryptocurrencies:") + for result in results_crypto["results"]: + logger.info(f" - {result['ticker']}: {result['display_name']}") + + # Demonstrate enhanced AKShare multi-market search + logger.info("\n=== Enhanced Multi-Market Search (AKShare) ===") + + # Search Hong Kong stocks + logger.info("Searching for Hong Kong stocks (Tencent)...") + hk_results = search_assets("00700", limit=3) + if hk_results["success"]: + for result in hk_results["results"]: + logger.info(f" - HK Stock: {result['ticker']}: {result['display_name']}") + + # Search US stocks through AKShare + logger.info("\nSearching for US stocks through AKShare...") + us_results = search_assets("AAPL", exchanges=["NASDAQ", "NYSE"], limit=3) + if us_results["success"]: + for result in us_results["results"]: + logger.info(f" - US Stock: {result['ticker']}: {result['display_name']}") + + # Direct ticker lookup fallback + logger.info("\nDemonstrating semantic search fallback...") + direct_results = search_assets( + "000001", limit=3 + ) # Should find through direct lookup + if direct_results["success"]: + for result in direct_results["results"]: + logger.info( + f" - Direct Match: {result['ticker']}: {result['display_name']}" + ) + + +def demonstrate_asset_info(): + """Demonstrate getting detailed asset information.""" + logger.info("\n=== Asset Information Demo ===") + + # Get info for Apple stock + tickers = ["NASDAQ:AAPL", "HKEX:700", "SSE:600519", "CRYPTO:BTC"] + + for ticker in tickers: + logger.info(f"\nGetting info for {ticker}...") + + # Get in English + info_en = get_asset_info(ticker, language="en-US") + if info_en["success"]: + logger.info( + f" English: {info_en['display_name']} " + f"({info_en['asset_type_display']})" + ) + logger.info(f" Exchange: {info_en['market_info']['exchange']}") + logger.info(f" Country: {info_en['market_info']['country']}") + + # Get in Chinese + info_zh = get_asset_info(ticker, language="zh-Hans") + if info_zh["success"]: + logger.info( + f" 中文: {info_zh['display_name']} ({info_zh['asset_type_display']})" + ) + + +def demonstrate_price_data(): + """Demonstrate real-time price data retrieval.""" + logger.info("\n=== Price Data Demo ===") + + tickers = ["NASDAQ:AAPL", "HKEX:700", "SSE:600519", "CRYPTO:BTC"] + + # Get individual price + logger.info("Getting individual price for AAPL...") + price_data = get_asset_price("NASDAQ:AAPL", language="en-US") + + if price_data["success"]: + logger.info(f" Price: {price_data['price_formatted']}") + if price_data["change_percent_formatted"]: + logger.info(f" Change: {price_data['change_percent_formatted']}") + if price_data["market_cap_formatted"]: + logger.info(f" Market Cap: {price_data['market_cap_formatted']}") + + # Get multiple prices + logger.info(f"\nGetting prices for multiple assets: {tickers}") + service = get_asset_service() + prices_data = service.get_multiple_prices(tickers, language="en-US") + + if prices_data["success"]: + logger.info(f"Successfully retrieved {prices_data['count']} prices:") + for ticker, price_info in prices_data["prices"].items(): + if price_info: + logger.info( + f" {ticker}: {price_info['price_formatted']} " + f"({price_info.get('change_percent_formatted', 'N/A')})" + ) + else: + logger.info(f" {ticker}: Price not available") + + +def demonstrate_watchlist_management(): + """Demonstrate watchlist creation and management.""" + logger.info("\n=== Watchlist Management Demo ===") + + user_id = "demo_user_123" + service = get_asset_service() + + # Create a watchlist + logger.info("Creating a new watchlist...") + create_result = service.create_watchlist( + user_id=user_id, + name="My Tech Stocks", + description="Technology companies I'm watching", + is_default=True, + ) + + if create_result["success"]: + logger.info("✓ Watchlist created successfully") + + # Add assets to watchlist + assets_to_add = [ + ("NASDAQ:AAPL", "Apple - iPhone maker"), + ("HKEX:700", "Tencent - Chinese tech giant"), + ("SSE:600519", "Kweichow Moutai - Chinese liquor company"), + ("CRYPTO:BTC", "Bitcoin - First and largest cryptocurrency"), + ] + + logger.info("Adding assets to watchlist...") + for ticker, notes in assets_to_add: + result = add_to_watchlist(user_id=user_id, ticker=ticker, notes=notes) + if result["success"]: + logger.info(f" ✓ Added {ticker}") + else: + logger.warning(f" ✗ Failed to add {ticker}: {result.get('error')}") + + # Get watchlist with prices + logger.info("\nRetrieving watchlist with current prices...") + watchlist_data = get_watchlist( + user_id=user_id, include_prices=True, language="zh-Hans" + ) + + if watchlist_data["success"]: + watchlist = watchlist_data["watchlist"] + logger.info(f"Watchlist: {watchlist['name']}") + logger.info(f"Number of assets: {watchlist['items_count']}") + + for asset in watchlist["assets"]: + display_name = asset["display_name"] + notes = asset["notes"] + + price_info = "" + if "price_data" in asset and asset["price_data"]: + price_data = asset["price_data"] + price_info = f" - {price_data['price_formatted']}" + if price_data.get("change_percent_formatted"): + price_info += f" ({price_data['change_percent_formatted']})" + + logger.info(f" • {display_name}{price_info}") + if notes: + logger.info(f" Notes: {notes}") + + # List all user watchlists + logger.info("\nListing all user watchlists...") + all_watchlists = service.get_user_watchlists(user_id) + + if all_watchlists["success"]: + logger.info(f"User {user_id} has {all_watchlists['count']} watchlists:") + for wl in all_watchlists["watchlists"]: + default_marker = " (Default)" if wl["is_default"] else "" + logger.info( + f" • {wl['name']}{default_marker} - {wl['items_count']} assets" + ) + + +def demonstrate_i18n_features(): + """Demonstrate internationalization features.""" + logger.info("\n=== Internationalization Demo ===") + + # Test different languages + languages = ["en-US", "zh-Hans", "zh-Hant"] + ticker = "NASDAQ:AAPL" + + for lang in languages: + logger.info(f"\nTesting language: {lang}") + + # Set language configuration + config = I18nConfig(language=lang) + set_i18n_config(config) + + # Search for assets + results = search_assets("APPL", language=lang, limit=1) + if results["success"] and results["results"]: + result = results["results"][0] + logger.info( + f" Search result: {result['display_name']} ({result['asset_type_display']})" + ) + + # Get price with localized formatting + price_data = get_asset_price(ticker, language=lang) + if price_data["success"]: + logger.info(f" Price: {price_data['price_formatted']}") + if price_data.get("change_percent_formatted"): + logger.info(f" Change: {price_data['change_percent_formatted']}") + + +def main(): + """Main demonstration function.""" + logger.info("=== ValueCell Asset Data Adapter Demo ===") + + try: + # Setup adapters + setup_adapters() + + # Run demonstrations + demonstrate_asset_search() + demonstrate_asset_info() + demonstrate_price_data() + demonstrate_watchlist_management() + demonstrate_i18n_features() + + logger.info("\n=== Demo completed successfully! ===") + + except Exception as e: + logger.error(f"Demo failed with error: {e}") + raise + + +if __name__ == "__main__": + main() diff --git a/python/valuecell/examples/i18n_example.py b/python/valuecell/examples/i18n_example.py new file mode 100644 index 000000000..9bd3dbd0e --- /dev/null +++ b/python/valuecell/examples/i18n_example.py @@ -0,0 +1,165 @@ +"""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/services/__init__.py b/python/valuecell/services/__init__.py new file mode 100644 index 000000000..b5f1f42cd --- /dev/null +++ b/python/valuecell/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/services/agent_context.py new file mode 100644 index 000000000..a82ba59b7 --- /dev/null +++ b/python/valuecell/services/agent_context.py @@ -0,0 +1,169 @@ +"""Agent context management for ValueCell application.""" + +from typing import Optional +from datetime import datetime +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 + + +class AgentContextManager: + """Manages context for agents to access user i18n settings.""" + + 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): + """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") + + # Update i18n service + self.i18n_service.set_language(self._local.language) + self.i18n_service.set_timezone(self._local.timezone) + + def get_current_user_id(self) -> Optional[str]: + """Get current user ID.""" + return getattr(self._local, "user_id", None) + + def get_current_session_id(self) -> Optional[str]: + """Get current session ID.""" + return getattr(self._local, "session_id", None) + + def get_current_language(self) -> str: + """Get current user's language.""" + return getattr(self._local, "language", "en-US") + + def get_current_timezone(self) -> str: + """Get current user's timezone.""" + return getattr(self._local, "timezone", "UTC") + + def get_i18n_context(self) -> AgentI18nContext: + """Get complete i18n context for agent.""" + return AgentI18nContext( + language=self.get_current_language(), + timezone=self.get_current_timezone(), + currency_symbol=self.i18n_service._i18n_config.get_currency_symbol(), + date_format=self.i18n_service._i18n_config.get_date_format(), + time_format=self.i18n_service._i18n_config.get_time_format(), + number_format=self.i18n_service._i18n_config.get_number_format(), + user_id=self.get_current_user_id(), + session_id=self.get_current_session_id(), + ) + + def translate(self, key: str, **variables) -> str: + """Translate using current user's language.""" + return self.i18n_service.translate( + key, self.get_current_language(), **variables + ) + + def format_datetime(self, dt: datetime, format_type: str = "datetime") -> str: + """Format datetime using current user's settings.""" + return self.i18n_service.format_datetime(dt, format_type) + + def format_number(self, number: float, decimal_places: int = 2) -> str: + """Format number using current user's settings.""" + return self.i18n_service.format_number(number, decimal_places) + + def format_currency(self, amount: float, decimal_places: int = 2) -> str: + """Format currency using current user's settings.""" + return self.i18n_service.format_currency(amount, decimal_places) + + @contextmanager + def user_context(self, user_id: str, session_id: Optional[str] = None): + """Context manager for temporary user context.""" + # Save current context + old_user_id = getattr(self._local, "user_id", None) + old_session_id = getattr(self._local, "session_id", None) + old_language = getattr(self._local, "language", "en-US") + old_timezone = getattr(self._local, "timezone", "UTC") + + try: + # Set new context + self.set_user_context(user_id, session_id) + yield self + finally: + # Restore old context + if old_user_id: + self._local.user_id = old_user_id + self._local.session_id = old_session_id + self._local.language = old_language + self._local.timezone = old_timezone + self.i18n_service.set_language(old_language) + self.i18n_service.set_timezone(old_timezone) + else: + # Clear context + if hasattr(self._local, "user_id"): + delattr(self._local, "user_id") + if hasattr(self._local, "session_id"): + delattr(self._local, "session_id") + if hasattr(self._local, "language"): + delattr(self._local, "language") + if hasattr(self._local, "timezone"): + delattr(self._local, "timezone") + + def clear_context(self): + """Clear current user context.""" + if hasattr(self._local, "user_id"): + delattr(self._local, "user_id") + if hasattr(self._local, "session_id"): + delattr(self._local, "session_id") + if hasattr(self._local, "language"): + delattr(self._local, "language") + if hasattr(self._local, "timezone"): + delattr(self._local, "timezone") + + +# Global agent context manager +_agent_context: Optional[AgentContextManager] = None + + +def get_agent_context() -> AgentContextManager: + """Get global agent context manager.""" + global _agent_context + if _agent_context is None: + _agent_context = AgentContextManager() + return _agent_context + + +def reset_agent_context(): + """Reset global agent context manager.""" + global _agent_context + _agent_context = None + + +# Convenience functions for agents +def set_user_context(user_id: str, session_id: Optional[str] = None): + """Set user context for current agent (convenience function).""" + return get_agent_context().set_user_context(user_id, session_id) + + +def get_current_user_id() -> Optional[str]: + """Get current user ID (convenience function).""" + return get_agent_context().get_current_user_id() + + +def get_i18n_context() -> AgentI18nContext: + """Get i18n context (convenience function).""" + return get_agent_context().get_i18n_context() + + +def t(key: str, **variables) -> str: + """Translate using current user context (convenience function).""" + return get_agent_context().translate(key, **variables) + + +def user_context(user_id: str, session_id: Optional[str] = None): + """Context manager for user context (convenience function).""" + return get_agent_context().user_context(user_id, session_id) diff --git a/python/valuecell/utils/i18n_utils.py b/python/valuecell/utils/i18n_utils.py new file mode 100644 index 000000000..e3cd657bf --- /dev/null +++ b/python/valuecell/utils/i18n_utils.py @@ -0,0 +1,426 @@ +"""Internationalization utility functions for ValueCell application.""" + +import re +from typing import Dict, List, Optional, Any +from datetime import datetime +import pytz +from pathlib import Path + +from ..core.constants import ( + SUPPORTED_LANGUAGE_CODES, + LANGUAGE_TIMEZONE_MAPPING, + DEFAULT_LANGUAGE, + DEFAULT_TIMEZONE, + SUPPORTED_LANGUAGES, +) +from ..services.i18n_service import get_i18n_service + + +def detect_browser_language(accept_language_header: str) -> str: + """Detect preferred language from browser Accept-Language header. + + Args: + accept_language_header: HTTP Accept-Language header value + + Returns: + Best matching supported language code + """ + if not accept_language_header: + return DEFAULT_LANGUAGE + + # Parse Accept-Language header + languages = [] + for item in accept_language_header.split(","): + parts = item.strip().split(";") + lang = parts[0].strip() + + # Extract quality value + quality = 1.0 + if len(parts) > 1: + q_part = parts[1].strip() + if q_part.startswith("q="): + try: + quality = float(q_part[2:]) + except ValueError: + quality = 1.0 + + languages.append((lang, quality)) + + # Sort by quality (descending) + languages.sort(key=lambda x: x[1], reverse=True) + + # Find best match + for lang, _ in languages: + # Direct match + if lang in SUPPORTED_LANGUAGE_CODES: + return lang + + # Try to match language family (e.g., 'zh' -> 'zh-Hans') + lang_family = lang.split("-")[0] + for supported_lang in SUPPORTED_LANGUAGE_CODES: + if supported_lang.startswith(lang_family): + return supported_lang + + return DEFAULT_LANGUAGE + + +def get_timezone_for_language(language: str) -> str: + """Get default timezone for a language. + + Args: + language: Language code + + Returns: + Timezone string + """ + return LANGUAGE_TIMEZONE_MAPPING.get(language, DEFAULT_TIMEZONE) + + +def validate_language_code(language: str) -> bool: + """Validate if language code is supported. + + Args: + language: Language code to validate + + Returns: + True if language is supported + """ + return language in SUPPORTED_LANGUAGE_CODES + + +def validate_timezone(timezone_str: str) -> bool: + """Validate if timezone string is valid. + + Args: + timezone_str: Timezone string to validate + + Returns: + True if timezone is valid + """ + try: + pytz.timezone(timezone_str) + return True + except pytz.UnknownTimeZoneError: + return False + + +def get_available_timezones() -> List[str]: + """Get list of all available timezones. + + Returns: + List of timezone strings + """ + return sorted(pytz.all_timezones) + + +def get_common_timezones() -> List[str]: + """Get list of commonly used timezones. + + Returns: + List of common timezone strings + """ + return sorted(pytz.common_timezones) + + +def get_timezone_display_name(timezone_str: str) -> str: + """Get display name for timezone. + + Args: + timezone_str: Timezone string + + Returns: + Human-readable timezone name + """ + try: + tz = pytz.timezone(timezone_str) + now = datetime.now(tz) + return f"{timezone_str} (UTC{now.strftime('%z')})" + except pytz.UnknownTimeZoneError: + return timezone_str + + +def convert_timezone(dt: datetime, from_tz: str, to_tz: str) -> datetime: + """Convert datetime from one timezone to another. + + Args: + dt: Datetime to convert + from_tz: Source timezone + to_tz: Target timezone + + Returns: + Converted datetime + """ + try: + from_timezone = pytz.timezone(from_tz) + to_timezone = pytz.timezone(to_tz) + + # Localize if naive + if dt.tzinfo is None: + dt = from_timezone.localize(dt) + + # Convert to target timezone + return dt.astimezone(to_timezone) + except pytz.UnknownTimeZoneError: + return dt + + +def format_file_size(size_bytes: int, language: Optional[str] = None) -> str: + """Format file size according to language settings. + + Args: + size_bytes: File size in bytes + language: Language code (uses current if not specified) + + Returns: + Formatted file size string + """ + i18n = get_i18n_service() + target_language = language or i18n.get_current_language() + + if size_bytes == 0: + return f"0 {i18n.translate('units.bytes', language=target_language)}" + + units = ["bytes", "kb", "mb", "gb", "tb"] + size = float(size_bytes) + unit_index = 0 + + while size >= 1024 and unit_index < len(units) - 1: + size /= 1024 + unit_index += 1 + + unit_key = f"units.{units[unit_index]}" + unit_name = i18n.translate(unit_key, language=target_language) + + if unit_index == 0: + return f"{int(size)} {unit_name}" + else: + formatted_size = i18n.format_number(size, 1) + return f"{formatted_size} {unit_name}" + + +def format_duration(seconds: int, language: Optional[str] = None) -> str: + """Format duration according to language settings. + + Args: + seconds: Duration in seconds + language: Language code (uses current if not specified) + + Returns: + Formatted duration string + """ + i18n = get_i18n_service() + target_language = language or i18n.get_current_language() + + if seconds < 60: + unit_name = i18n.translate("units.seconds", language=target_language) + return f"{seconds} {unit_name}" + elif seconds < 3600: + minutes = seconds // 60 + unit_name = i18n.translate("units.minutes", language=target_language) + return f"{minutes} {unit_name}" + elif seconds < 86400: + hours = seconds // 3600 + unit_name = i18n.translate("units.hours", language=target_language) + return f"{hours} {unit_name}" + else: + days = seconds // 86400 + unit_name = i18n.translate("units.days", language=target_language) + return f"{days} {unit_name}" + + +def pluralize( + count: int, + singular: str, + plural: Optional[str] = None, + language: Optional[str] = None, +) -> str: + """Pluralize a word based on count and language rules. + + Args: + count: Number to determine plural form + singular: Singular form of the word + plural: Plural form (auto-generated if not provided) + language: Language code (uses current if not specified) + + Returns: + Appropriate word form + """ + target_language = language or get_i18n_service().get_current_language() + + # Chinese languages don't have plural forms + if target_language.startswith("zh"): + return singular + + # English pluralization rules + if count == 1: + return singular + + if plural: + return plural + + # Simple English pluralization + if singular.endswith(("s", "sh", "ch", "x", "z")): + return f"{singular}es" + elif singular.endswith("y") and singular[-2] not in "aeiou": + return f"{singular[:-1]}ies" + elif singular.endswith("f"): + return f"{singular[:-1]}ves" + elif singular.endswith("fe"): + return f"{singular[:-2]}ves" + else: + return f"{singular}s" + + +def get_language_direction(language: str) -> str: + """Get text direction for a language. + + Args: + language: Language code + + Returns: + 'ltr' for left-to-right, 'rtl' for right-to-left + """ + # All currently supported languages are LTR + return "ltr" + + +def extract_translation_keys(text: str) -> List[str]: + """Extract translation keys from text using t() function calls. + + Args: + text: Text to extract keys from + + Returns: + List of translation keys found + """ + # Pattern to match t('key') or t("key") calls + pattern = r't\([\'"]([^\'"]+)[\'"]\)' + matches = re.findall(pattern, text) + return list(set(matches)) # Remove duplicates + + +def validate_translation_file(file_path: Path) -> Dict[str, Any]: + """Validate a translation JSON file. + + Args: + file_path: Path to translation file + + Returns: + Validation result with status and errors + """ + result = {"valid": True, "errors": [], "warnings": [], "key_count": 0} + + try: + import json + + if not file_path.exists(): + result["valid"] = False + result["errors"].append("File does not exist") + return result + + with open(file_path, "r", encoding="utf-8") as f: + data = json.load(f) + + # Count keys recursively + def count_keys(obj): + count = 0 + if isinstance(obj, dict): + for key, value in obj.items(): + if isinstance(value, dict): + count += count_keys(value) + else: + count += 1 + return count + + result["key_count"] = count_keys(data) + + # Check for empty values + def check_empty_values(obj, prefix=""): + for key, value in obj.items(): + current_key = f"{prefix}.{key}" if prefix else key + if isinstance(value, dict): + check_empty_values(value, current_key) + elif not value or (isinstance(value, str) and not value.strip()): + result["warnings"].append(f"Empty value for key: {current_key}") + + check_empty_values(data) + + except json.JSONDecodeError as e: + result["valid"] = False + result["errors"].append(f"Invalid JSON: {str(e)}") + except Exception as e: + result["valid"] = False + result["errors"].append(f"Error reading file: {str(e)}") + + return result + + +def get_missing_translations(base_language: str = "en-US") -> Dict[str, List[str]]: + """Find missing translations compared to base language. + + Args: + base_language: Base language to compare against + + Returns: + Dictionary with missing keys for each language + """ + i18n = get_i18n_service() + base_keys = set(i18n.get_translation_keys(base_language)) + missing = {} + + for lang_code, _ in SUPPORTED_LANGUAGES: + if lang_code == base_language: + continue + + lang_keys = set(i18n.get_translation_keys(lang_code)) + missing_keys = base_keys - lang_keys + + if missing_keys: + missing[lang_code] = sorted(list(missing_keys)) + + return missing + + +def create_translation_template(keys: List[str]) -> Dict[str, Any]: + """Create a translation template with given keys. + + Args: + keys: List of translation keys + + Returns: + Nested dictionary template + """ + template = {} + + for key in keys: + parts = key.split(".") + current = template + + for i, part in enumerate(parts): + if i == len(parts) - 1: + # Last part, set empty string + current[part] = "" + else: + # Create nested dict if doesn't exist + if part not in current: + current[part] = {} + current = current[part] + + return template + + +# Decorator for translatable strings +def translatable(key: str, **kwargs): + """Decorator to mark functions as translatable. + + Args: + key: Translation key + **kwargs: Additional translation parameters + """ + + def decorator(func): + func._translation_key = key + func._translation_params = kwargs + return func + + return decorator From 4c4357bd49ce5078cc6fb7dee0d4eb7ae5403b20 Mon Sep 17 00:00:00 2001 From: zhonghao lu Date: Mon, 15 Sep 2025 15:53:38 +0800 Subject: [PATCH 07/10] recovery file --- python/valuecell/i18n.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/python/valuecell/i18n.py b/python/valuecell/i18n.py index 04a173773..d09924045 100644 --- a/python/valuecell/i18n.py +++ b/python/valuecell/i18n.py @@ -4,7 +4,13 @@ Import from here to access all i18n features in one place. """ -# Core i18n functionality removed +# Core i18n functionality +from .services.i18n_service import ( + get_i18n_service, + t, + translate, + reset_i18n_service, +) # Configuration from .config.settings import get_settings From 7c41bb011eaa9b3471f14bf67e399b26cc6ae89c Mon Sep 17 00:00:00 2001 From: zhonghao lu Date: Mon, 15 Sep 2025 15:54:49 +0800 Subject: [PATCH 08/10] remove useless code --- python/valuecell/server/services/__init__.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/python/valuecell/server/services/__init__.py b/python/valuecell/server/services/__init__.py index 75bf3b085..e69de29bb 100644 --- a/python/valuecell/server/services/__init__.py +++ b/python/valuecell/server/services/__init__.py @@ -1,11 +0,0 @@ -"""Services for ValueCell Server.""" - -from .agents.agent_service import AgentService -from .assets.asset_service import AssetService -from .i18n.i18n_service import I18nService - -__all__ = [ - "AgentService", - "AssetService", - "I18nService", -] \ No newline at end of file From 3835077c900ef5d608c3790435fbaf071a2ead04 Mon Sep 17 00:00:00 2001 From: zhonghao lu Date: Mon, 15 Sep 2025 15:56:18 +0800 Subject: [PATCH 09/10] remove useless code --- python/valuecell/server/api/app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/valuecell/server/api/app.py b/python/valuecell/server/api/app.py index 34561f4dc..64b0905b5 100644 --- a/python/valuecell/server/api/app.py +++ b/python/valuecell/server/api/app.py @@ -53,5 +53,5 @@ def _add_middleware(app: FastAPI, settings) -> None: def _add_routes(app: FastAPI) -> 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"]) \ No newline at end of file + # app.include_router(health.router, prefix="/health", tags=["health"]) + # app.include_router(agents.router, prefix="/api/v1", tags=["agents"]) \ No newline at end of file From 78f40119c5580644ad14b4b05d22e006ff43c21d Mon Sep 17 00:00:00 2001 From: zhonghao lu Date: Mon, 15 Sep 2025 15:58:13 +0800 Subject: [PATCH 10/10] remove useless code --- python/valuecell/server/api/app.py | 18 +++++----- python/valuecell/server/config/settings.py | 34 ++++++++++--------- python/valuecell/server/db/models/__init__.py | 4 +-- python/valuecell/server/db/models/base.py | 2 +- python/valuecell/server/main.py | 4 +-- 5 files changed, 32 insertions(+), 30 deletions(-) diff --git a/python/valuecell/server/api/app.py b/python/valuecell/server/api/app.py index 64b0905b5..09c050997 100644 --- a/python/valuecell/server/api/app.py +++ b/python/valuecell/server/api/app.py @@ -10,15 +10,17 @@ def create_app() -> FastAPI: """Create and configure FastAPI application.""" settings = get_settings() - + @asynccontextmanager async def lifespan(app: FastAPI): # Startup - print(f"ValueCell Server starting up on {settings.API_HOST}:{settings.API_PORT}...") + print( + f"ValueCell Server starting up on {settings.API_HOST}:{settings.API_PORT}..." + ) yield # Shutdown print("ValueCell Server shutting down...") - + app = FastAPI( title="ValueCell Server API", description="A community-driven, multi-agent platform for financial applications", @@ -27,13 +29,13 @@ async def lifespan(app: FastAPI): docs_url="/docs" if settings.API_DEBUG else None, redoc_url="/redoc" if settings.API_DEBUG else None, ) - + # Add middleware _add_middleware(app, settings) - + # Add routes _add_routes(app) - + return app @@ -47,11 +49,11 @@ def _add_middleware(app: FastAPI, settings) -> None: allow_methods=["*"], allow_headers=["*"], ) - + # Custom logging middleware removed def _add_routes(app: FastAPI) -> 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"]) \ No newline at end of file + # app.include_router(agents.router, prefix="/api/v1", tags=["agents"]) diff --git a/python/valuecell/server/config/settings.py b/python/valuecell/server/config/settings.py index fb71c8f03..2c21b775b 100644 --- a/python/valuecell/server/config/settings.py +++ b/python/valuecell/server/config/settings.py @@ -7,69 +7,71 @@ class Settings: """Server configuration settings.""" - + def __init__(self): """Initialize settings from environment variables.""" # Application Configuration self.APP_NAME = os.getenv("APP_NAME", "ValueCell Server") 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" - + # CORS Configuration cors_origins = os.getenv("CORS_ORIGINS", "*") self.CORS_ORIGINS = cors_origins.split(",") if cors_origins != "*" else ["*"] - + # Database Configuration self.DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./valuecell.db") self.DB_ECHO = os.getenv("DB_ECHO", "false").lower() == "true" - + # Redis Configuration self.REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379") - + # Security Configuration self.SECRET_KEY = os.getenv("SECRET_KEY", "your-secret-key-here") - self.ACCESS_TOKEN_EXPIRE_MINUTES = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "30")) - + self.ACCESS_TOKEN_EXPIRE_MINUTES = int( + os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "30") + ) + # Logging Configuration self.LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO") self.LOG_FORMAT = os.getenv("LOG_FORMAT", "json") - + # File Paths self.BASE_DIR = Path(__file__).parent.parent.parent self.LOGS_DIR = self.BASE_DIR / "logs" self.LOGS_DIR.mkdir(exist_ok=True) - + # Agent Configuration self.AGENT_TIMEOUT = int(os.getenv("AGENT_TIMEOUT", "300")) # 5 minutes self.MAX_CONCURRENT_AGENTS = int(os.getenv("MAX_CONCURRENT_AGENTS", "10")) - + # External APIs self.OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") self.FINNHUB_API_KEY = os.getenv("FINNHUB_API_KEY") self.ALPHA_VANTAGE_API_KEY = os.getenv("ALPHA_VANTAGE_API_KEY") - + @property def is_development(self) -> bool: """Check if running in development mode.""" return self.APP_ENVIRONMENT == "development" - + @property def is_production(self) -> bool: """Check if running in production mode.""" return self.APP_ENVIRONMENT == "production" - + def get_database_config(self) -> dict: """Get database configuration.""" return { "url": self.DATABASE_URL, "echo": self.DB_ECHO, } - + def get_redis_config(self) -> dict: """Get Redis configuration.""" return { @@ -80,4 +82,4 @@ def get_redis_config(self) -> dict: @lru_cache() def get_settings() -> Settings: """Get cached settings instance.""" - return Settings() \ No newline at end of file + return Settings() diff --git a/python/valuecell/server/db/models/__init__.py b/python/valuecell/server/db/models/__init__.py index e21a2308f..7fde07179 100644 --- a/python/valuecell/server/db/models/__init__.py +++ b/python/valuecell/server/db/models/__init__.py @@ -2,6 +2,4 @@ from .base import Base -__all__ = [ - "Base" -] \ No newline at end of file +__all__ = ["Base"] diff --git a/python/valuecell/server/db/models/base.py b/python/valuecell/server/db/models/base.py index 0f1b2a96b..2efbec3f2 100644 --- a/python/valuecell/server/db/models/base.py +++ b/python/valuecell/server/db/models/base.py @@ -7,4 +7,4 @@ # Alternative approach using modern SQLAlchemy 2.0 style # class Base(DeclarativeBase): -# pass \ No newline at end of file +# pass diff --git a/python/valuecell/server/main.py b/python/valuecell/server/main.py index 23469b69c..826a100cd 100644 --- a/python/valuecell/server/main.py +++ b/python/valuecell/server/main.py @@ -9,7 +9,7 @@ def main(): """Start the server.""" settings = get_settings() app = create_app() - + uvicorn.run( app, host=settings.API_HOST, @@ -19,4 +19,4 @@ def main(): if __name__ == "__main__": - main() \ No newline at end of file + main()