diff --git a/python/scripts/init_database.py b/python/scripts/init_database.py new file mode 100644 index 000000000..a38f25ced --- /dev/null +++ b/python/scripts/init_database.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 +"""Standalone database initialization script for ValueCell.""" + +import sys +from pathlib import Path + + +def setup_path_and_run(): + """Setup Python path and run the database initialization.""" + # Add the project root to Python path + project_root = Path(__file__).parent.parent + sys.path.insert(0, str(project_root)) + + # Import after path setup to avoid import errors + import valuecell.server.db.init_db as init_db_module + + # Run the main function + init_db_module.main() + + +if __name__ == "__main__": + setup_path_and_run() diff --git a/python/valuecell/server/db/README.md b/python/valuecell/server/db/README.md new file mode 100644 index 000000000..f75aff3a9 --- /dev/null +++ b/python/valuecell/server/db/README.md @@ -0,0 +1,253 @@ +# ValueCell Database Initialization + +This directory contains database-related code for ValueCell Server, including model definitions, connection management, and initialization scripts. + +## Directory Structure + +``` +db/ +├── __init__.py # Database package initialization +├── connection.py # Database connection and session management +├── init_db.py # Database initialization script +├── models/ # Database models +│ ├── __init__.py +│ ├── base.py # Base model class +│ ├── agent.py # Agent model +│ └── asset.py # Asset model +└── README.md # This document +``` + +## Database Configuration + +Database configuration is defined in `valuecell/server/config/settings.py`: + +- **DATABASE_URL**: Database connection URL, defaults to `sqlite:///./valuecell.db` +- **DB_ECHO**: Whether to output SQL logs, defaults to `false` + +## Database Models + +### Agent Model (Agent) +The Agent model stores information about all available AI agents in the ValueCell system: + +**Basic Information**: +- `name`: Unique agent identifier +- `display_name`: Human-readable display name +- `description`: Detailed description of agent functionality and purpose +- `version`: Agent version number + +**State Management**: +- `enabled`: Whether the agent is enabled +- `is_active`: Whether the agent is active and available + +**Functionality**: +- `capabilities`: JSON format capability description (e.g., streaming, push notifications) +- `agent_metadata`: Additional metadata (author, tags, supported features, etc.) +- `config`: Agent-specific configuration parameters + +**Timestamps**: +- `created_at`: Creation time +- `updated_at`: Update time + +### Asset Model (Asset) +The Asset model represents financial assets in the ValueCell system, including stocks, bonds, cryptocurrencies, and other investment instruments. + +**Key fields**: +- `id`: Primary key (auto-increment) +- `symbol`: Unique asset symbol/ticker (e.g., "AAPL", "BTC-USD") +- `name`: Full name of the asset +- `description`: Detailed description of the asset +- `asset_type`: Type of asset (stock, bond, crypto, commodity, etc.) +- `sector`: Industry sector (for stocks) +- `current_price`: Current market price +- `is_active`: Whether the asset is active +- `asset_metadata`: Additional metadata (fundamental data, tags, etc.) +- `config`: Asset-specific configuration parameters +- `created_at`: Timestamp when the asset was created +- `updated_at`: Timestamp when the asset was last updated + +## Database Initialization + +### Usage + +1. **Basic initialization**: + ```bash + cd /path/to/valuecell/python + python3 -m valuecell.server.db.init_db + ``` + +2. **Force re-initialization**: + ```bash + python3 -m valuecell.server.db.init_db --force + ``` + +3. **Verbose logging**: + ```bash + python3 -m valuecell.server.db.init_db --verbose + ``` + +4. **Using standalone script**: + ```bash + python3 scripts/init_database.py + ``` + +### Initialization Process + +1. **Check database file**: Verify if SQLite database file exists +2. **Create database file**: Create new database file if it doesn't exist +3. **Create table structure**: Create agents and assets tables based on model definitions +4. **Initialize default data**: + - **Agent data**: Insert default Agent records directly from code + - Create three default agents: AIHedgeFundAgent, Sec13FundAgent, and TradingAgentsAdapter + - Support updating existing Agent configuration information + - **Asset data**: Insert default Asset records for common financial instruments + - Create default assets: AAPL, GOOGL, MSFT, SPY, BTC-USD + - Support updating existing Asset information +5. **Verify initialization**: Confirm database connection and table structure are correct + +### Default Records + +The initialization script automatically creates default records directly in the code: + +#### Default Agents + +**Default agents created**: + +1. **AIHedgeFundAgent**: AI-powered hedge fund analysis and trading agent +2. **Sec13FundAgent**: SEC 13F fund analysis and tracking agent +3. **TradingAgentsAdapter**: Multi-agent trading analysis system with market, sentiment, news and fundamentals analysis + +#### Default Assets + +**Default assets created**: + +1. **AAPL**: Apple Inc. (Technology stock) +2. **GOOGL**: Alphabet Inc. Class A (Technology stock) +3. **MSFT**: Microsoft Corporation (Technology stock) +4. **SPY**: SPDR S&P 500 ETF Trust (Index ETF) +5. **BTC-USD**: Bitcoin (Cryptocurrency) + +**Agent data structure example**: +```python +{ + "name": "TradingAgentsAdapter", + "display_name": "Trading Agents Adapter", + "description": "TradingAgents - Multi-agent trading analysis system", + "version": "1.0.0", + "enabled": True, + "is_active": True, + "capabilities": { + "streaming": True, + "push_notifications": False + }, + "metadata": { + "version": "1.0.0", + "author": "ValueCell Team", + "tags": ["trading", "analysis", "multi-agent"], + "supported_tickers": ["AAPL", "GOOGL", "MSFT"], + "supported_analysts": ["market", "social", "news"] + } +} +``` + +**Asset data structure example**: +```python +{ + "symbol": "AAPL", + "name": "Apple Inc.", + "asset_type": "stock", + "sector": "Technology", + "is_active": True, + "metadata": { + "market_cap": "large", + "dividend_yield": 0.5, + "beta": 1.2, + "tags": ["blue-chip", "dividend", "growth"] + } +} +``` + +## Usage in Code + +### Getting Database Session + +```python +from valuecell.server.db import get_db, Agent, Asset + +# Using dependency injection in FastAPI routes +@app.get("/api/agents") +def get_agents(db: Session = Depends(get_db)): + return db.query(Agent).filter(Agent.enabled == True).all() + +@app.get("/api/assets") +def get_assets(db: Session = Depends(get_db)): + return db.query(Asset).filter(Asset.is_active == True).all() +``` + +### Direct Database Manager Usage + +```python +from valuecell.server.db import get_database_manager, Agent + +db_manager = get_database_manager() +session = db_manager.get_session() + +try: + # Get all enabled agents + agents = session.query(Agent).filter(Agent.enabled == True).all() + + # Get specific agent + trading_agent = session.query(Agent).filter(Agent.name == "TradingAgentsAdapter").first() + + # Update agent status + if trading_agent: + trading_agent.is_active = True + session.commit() +finally: + session.close() +``` + +### Programmatic Initialization + +```python +from valuecell.server.db import init_database + +# Initialize database +success = init_database(force=False) +if success: + print("Database initialization successful") +else: + print("Database initialization failed") +``` + +## Important Notes + +1. **Password Security**: Default admin user password is a placeholder and should be replaced with proper hashed password in production environment +2. **Database Backup**: SQLite database file should be backed up regularly +3. **Permission Management**: Ensure database file has appropriate filesystem permissions +4. **Environment Variables**: Database connection can be customized through `DATABASE_URL` environment variable + +## Troubleshooting + +### Common Issues + +1. **Permission Error**: Ensure write permissions to database file directory +2. **Module Import Error**: Ensure running in correct Python environment +3. **Database Lock**: Ensure no other processes are using the database file + +### Reset Database + +If you need to completely reset the database: + +```bash +# Delete existing database file +rm valuecell.db + +# Re-initialize +python3 -m valuecell.server.db.init_db +``` + +Or use force re-initialization: + +```bash +python3 -m valuecell.server.db.init_db --force +``` \ No newline at end of file diff --git a/python/valuecell/server/db/__init__.py b/python/valuecell/server/db/__init__.py index e69de29bb..a18580123 100644 --- a/python/valuecell/server/db/__init__.py +++ b/python/valuecell/server/db/__init__.py @@ -0,0 +1,23 @@ +"""Database package for ValueCell Server.""" + +from .connection import ( + DatabaseManager, + get_database_manager, + get_db, +) +from .init_db import DatabaseInitializer, init_database +from .models import Base, Agent, Asset + +__all__ = [ + # Connection management + "DatabaseManager", + "get_database_manager", + "get_db", + # Database initialization + "DatabaseInitializer", + "init_database", + # Models + "Base", + "Agent", + "Asset", +] diff --git a/python/valuecell/server/db/connection.py b/python/valuecell/server/db/connection.py new file mode 100644 index 000000000..f70c4efcf --- /dev/null +++ b/python/valuecell/server/db/connection.py @@ -0,0 +1,102 @@ +"""Database connection and session management for ValueCell Server.""" + +from typing import Generator +from sqlalchemy import create_engine, Engine +from sqlalchemy.orm import sessionmaker, Session +from sqlalchemy.pool import StaticPool + +from ..config.settings import get_settings +from .models.base import Base + + +class DatabaseManager: + """Database connection and session manager.""" + + def __init__(self): + """Initialize database manager.""" + self.settings = get_settings() + self.engine: Engine = None + self.SessionLocal = None + self._initialize_engine() + + def _initialize_engine(self) -> None: + """Initialize database engine.""" + database_config = self.settings.get_database_config() + + # SQLite specific configuration + connect_args = {} + if database_config["url"].startswith("sqlite"): + connect_args = { + "check_same_thread": False, + "timeout": 20, + } + + self.engine = create_engine( + database_config["url"], + echo=database_config["echo"], + connect_args=connect_args, + poolclass=StaticPool + if database_config["url"].startswith("sqlite") + else None, + ) + + self.SessionLocal = sessionmaker( + autocommit=False, autoflush=False, bind=self.engine + ) + + def get_engine(self) -> Engine: + """Get database engine.""" + return self.engine + + def create_tables(self) -> None: + """Create all tables defined in models.""" + Base.metadata.create_all(bind=self.engine) + + def drop_tables(self) -> None: + """Drop all tables.""" + Base.metadata.drop_all(bind=self.engine) + + def get_session(self) -> Session: + """Get a new database session.""" + return self.SessionLocal() + + def get_db_session(self) -> Generator[Session, None, None]: + """Get database session for dependency injection.""" + db = self.SessionLocal() + try: + yield db + finally: + db.close() + + +# Global database manager instance +_db_manager: DatabaseManager = None + + +def get_database_manager() -> DatabaseManager: + """Get global database manager instance.""" + global _db_manager + if _db_manager is None: + _db_manager = DatabaseManager() + return _db_manager + + +def get_db() -> Generator[Session, None, None]: + """Get database session for FastAPI dependency injection.""" + db_manager = get_database_manager() + yield from db_manager.get_db_session() + + +def get_engine() -> Engine: + """Get database engine.""" + return get_database_manager().get_engine() + + +def create_tables() -> None: + """Create all database tables.""" + get_database_manager().create_tables() + + +def drop_tables() -> None: + """Drop all database tables.""" + get_database_manager().drop_tables() diff --git a/python/valuecell/server/db/init_db.py b/python/valuecell/server/db/init_db.py new file mode 100644 index 000000000..9861ac077 --- /dev/null +++ b/python/valuecell/server/db/init_db.py @@ -0,0 +1,470 @@ +"""Database initialization script for ValueCell Server.""" + +import logging +import sys +from pathlib import Path +from typing import Optional + +from sqlalchemy import text, inspect +from sqlalchemy.exc import SQLAlchemyError + +from .connection import get_database_manager, DatabaseManager +from .models.base import Base +from ..config.settings import get_settings + +# Configure logging +logging.basicConfig( + level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" +) +logger = logging.getLogger(__name__) + + +class DatabaseInitializer: + """Database initialization manager.""" + + def __init__(self, db_manager: Optional[DatabaseManager] = None): + """Initialize database initializer.""" + self.db_manager = db_manager or get_database_manager() + self.settings = get_settings() + self.engine = self.db_manager.get_engine() + + def check_database_exists(self) -> bool: + """Check if database file exists (for SQLite).""" + database_url = self.settings.DATABASE_URL + + if database_url.startswith("sqlite:///"): + # Extract file path from SQLite URL + db_path = database_url.replace("sqlite:///", "") + if db_path.startswith("./"): + # Relative path + db_path = Path.cwd() / db_path[2:] + else: + db_path = Path(db_path) + + return db_path.exists() + + # For other databases, try to connect + try: + with self.engine.connect() as conn: + conn.execute(text("SELECT 1")) + return True + except SQLAlchemyError: + return False + + def check_tables_exist(self) -> bool: + """Check if tables exist in database.""" + try: + inspector = inspect(self.engine) + existing_tables = inspector.get_table_names() + + # Get all table names from metadata + expected_tables = list(Base.metadata.tables.keys()) + + if not expected_tables: + logger.info("No tables defined in models") + return True + + # Check if all expected tables exist + missing_tables = set(expected_tables) - set(existing_tables) + if missing_tables: + logger.info(f"Missing tables: {missing_tables}") + return False + + logger.info(f"All tables exist: {existing_tables}") + return True + + except SQLAlchemyError as e: + logger.error(f"Error checking tables: {e}") + return False + + def create_database_file(self) -> bool: + """Create database file (for SQLite).""" + database_url = self.settings.DATABASE_URL + + if database_url.startswith("sqlite:///"): + # Extract file path from SQLite URL + db_path = database_url.replace("sqlite:///", "") + if db_path.startswith("./"): + # Relative path + db_path = Path.cwd() / db_path[2:] + else: + db_path = Path(db_path) + + try: + # Create parent directories if they don't exist + db_path.parent.mkdir(parents=True, exist_ok=True) + + # Create empty database file + db_path.touch() + logger.info(f"Created database file: {db_path}") + return True + + except Exception as e: + logger.error(f"Error creating database file: {e}") + return False + + logger.info("Database file creation not needed for non-SQLite databases") + return True + + def create_tables(self) -> bool: + """Create all tables.""" + try: + logger.info("Creating database tables...") + Base.metadata.create_all(bind=self.engine) + logger.info("Database tables created successfully") + return True + + except SQLAlchemyError as e: + logger.error(f"Error creating tables: {e}") + return False + + def initialize_basic_data(self) -> bool: + """Initialize default agent data.""" + try: + logger.info("Initializing default agent data...") + + # Get a database session + session = self.db_manager.get_session() + + try: + # Import models here to avoid circular imports + from .models.agent import Agent + from .models.asset import Asset + + # Define default agents + default_agents = [ + { + "name": "AIHedgeFundAgent", + "display_name": "AI Hedge Fund Agent", + "description": "AI-powered hedge fund analysis and trading agent", + "version": "1.0.0", + "enabled": True, + "is_active": True, + "capabilities": { + "streaming": False, + "push_notifications": False, + }, + "metadata": { + "version": "1.0.0", + "author": "ValueCell Team", + "tags": ["hedge-fund", "ai", "trading"], + }, + }, + { + "name": "Sec13FundAgent", + "display_name": "SEC 13F Fund Agent", + "description": "SEC 13F fund analysis and tracking agent", + "version": "1.0.0", + "enabled": True, + "is_active": True, + "capabilities": { + "streaming": False, + "push_notifications": False, + }, + "metadata": { + "version": "1.0.0", + "author": "ValueCell Team", + "tags": ["sec", "13f", "fund-analysis"], + }, + }, + { + "name": "TradingAgentsAdapter", + "display_name": "Trading Agents Adapter", + "description": "TradingAgents - Multi-agent trading analysis system with market, sentiment, news and fundamentals analysis", + "version": "1.0.0", + "enabled": True, + "is_active": True, + "capabilities": { + "streaming": True, + "push_notifications": False, + }, + "metadata": { + "version": "1.0.0", + "author": "ValueCell Team", + "tags": [ + "trading", + "analysis", + "multi-agent", + "stocks", + "finance", + ], + "supported_tickers": [ + "AAPL", + "GOOGL", + "MSFT", + "NVDA", + "TSLA", + "AMZN", + "META", + "NFLX", + "SPY", + ], + "supported_analysts": [ + "market", + "social", + "news", + "fundamentals", + ], + "supported_llm_providers": [ + "openai", + "anthropic", + "google", + "ollama", + "openrouter", + ], + }, + }, + ] + + # Insert default agents + for agent_data in default_agents: + agent_name = agent_data["name"] + + # Check if agent already exists + existing_agent = ( + session.query(Agent).filter_by(name=agent_name).first() + ) + + if not existing_agent: + # Create new agent + agent = Agent.from_config(agent_data) + session.add(agent) + logger.info(f"Added default agent: {agent_name}") + else: + # Update existing agent with default data + existing_agent.display_name = agent_data.get( + "display_name", existing_agent.display_name + ) + existing_agent.description = agent_data.get( + "description", existing_agent.description + ) + existing_agent.version = agent_data.get( + "version", existing_agent.version + ) + existing_agent.enabled = agent_data.get( + "enabled", existing_agent.enabled + ) + existing_agent.is_active = agent_data.get( + "is_active", existing_agent.is_active + ) + existing_agent.capabilities = agent_data.get( + "capabilities", existing_agent.capabilities + ) + existing_agent.agent_metadata = agent_data.get( + "metadata", existing_agent.agent_metadata + ) + existing_agent.config = agent_data.get( + "config", existing_agent.config + ) + logger.info(f"Updated default agent: {agent_name}") + + # Define default assets + default_assets = [ + { + "symbol": "AAPL", + "name": "Apple Inc.", + "asset_type": "stock", + "sector": "Technology", + "is_active": True, + "metadata": { + "market_cap": "large", + "dividend_yield": 0.5, + "beta": 1.2, + "tags": ["blue-chip", "dividend", "growth"], + }, + }, + { + "symbol": "GOOGL", + "name": "Alphabet Inc. Class A", + "asset_type": "stock", + "sector": "Technology", + "is_active": True, + "metadata": { + "market_cap": "large", + "dividend_yield": 0.0, + "beta": 1.1, + "tags": ["growth", "tech-giant", "ai"], + }, + }, + { + "symbol": "MSFT", + "name": "Microsoft Corporation", + "asset_type": "stock", + "sector": "Technology", + "is_active": True, + "metadata": { + "market_cap": "large", + "dividend_yield": 0.7, + "beta": 0.9, + "tags": ["blue-chip", "dividend", "cloud", "ai"], + }, + }, + { + "symbol": "SPY", + "name": "SPDR S&P 500 ETF Trust", + "asset_type": "etf", + "sector": "Diversified", + "is_active": True, + "metadata": { + "expense_ratio": 0.0945, + "aum": "400B+", + "tags": ["index", "diversified", "low-cost"], + }, + }, + { + "symbol": "BTC-USD", + "name": "Bitcoin", + "asset_type": "cryptocurrency", + "sector": "Cryptocurrency", + "is_active": True, + "metadata": { + "market_cap": "large", + "volatility": "high", + "tags": ["crypto", "store-of-value", "digital-gold"], + }, + }, + ] + + # Insert default assets + for asset_data in default_assets: + asset_symbol = asset_data["symbol"] + + # Check if asset already exists + existing_asset = ( + session.query(Asset).filter_by(symbol=asset_symbol).first() + ) + + if not existing_asset: + # Create new asset + asset = Asset.from_config(asset_data) + session.add(asset) + logger.info(f"Added default asset: {asset_symbol}") + else: + # Update existing asset with default data + existing_asset.name = asset_data.get( + "name", existing_asset.name + ) + existing_asset.asset_type = asset_data.get( + "asset_type", existing_asset.asset_type + ) + existing_asset.sector = asset_data.get( + "sector", existing_asset.sector + ) + existing_asset.is_active = asset_data.get( + "is_active", existing_asset.is_active + ) + existing_asset.asset_metadata = asset_data.get( + "metadata", existing_asset.asset_metadata + ) + logger.info(f"Updated default asset: {asset_symbol}") + + session.commit() + logger.info("Default agent and asset data initialization completed") + return True + + except Exception as e: + session.rollback() + logger.error(f"Error initializing default agent data: {e}") + return False + finally: + session.close() + + except Exception as e: + logger.error(f"Error getting database session: {e}") + return False + + def verify_initialization(self) -> bool: + """Verify database initialization.""" + try: + # Test database connection + with self.engine.connect() as conn: + result = conn.execute(text("SELECT 1")) + result.fetchone() + + # Check if tables exist + if not self.check_tables_exist(): + logger.error("Table verification failed") + return False + + logger.info("Database initialization verified successfully") + return True + + except SQLAlchemyError as e: + logger.error(f"Database verification failed: {e}") + return False + + def initialize(self, force: bool = False) -> bool: + """Initialize database completely.""" + logger.info("Starting database initialization...") + + # Check if database already exists and is properly initialized + if not force and self.check_database_exists() and self.check_tables_exist(): + logger.info("Database already exists and is properly initialized") + return True + + # Step 1: Create database file (for SQLite) + if not self.create_database_file(): + logger.error("Failed to create database file") + return False + + # Step 2: Create tables + if not self.create_tables(): + logger.error("Failed to create tables") + return False + + # Step 3: Initialize basic data + if not self.initialize_basic_data(): + logger.error("Failed to initialize basic data") + return False + + # Step 4: Verify initialization + if not self.verify_initialization(): + logger.error("Database initialization verification failed") + return False + + logger.info("Database initialization completed successfully") + return True + + +def init_database(force: bool = False) -> bool: + """Initialize database with all tables and basic data.""" + try: + initializer = DatabaseInitializer() + return initializer.initialize(force=force) + except Exception as e: + logger.error(f"Database initialization failed: {e}") + return False + + +def main(): + """Main entry point for database initialization script.""" + import argparse + + parser = argparse.ArgumentParser(description="Initialize ValueCell database") + parser.add_argument( + "--force", + action="store_true", + help="Force re-initialization even if database exists", + ) + parser.add_argument("--verbose", action="store_true", help="Enable verbose logging") + + args = parser.parse_args() + + if args.verbose: + logging.getLogger().setLevel(logging.DEBUG) + + logger.info("ValueCell Database Initialization") + logger.info("=" * 50) + + success = init_database(force=args.force) + + if success: + logger.info("Database initialization completed successfully!") + sys.exit(0) + else: + logger.error("Database initialization failed!") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/python/valuecell/server/db/models/__init__.py b/python/valuecell/server/db/models/__init__.py index 7fde07179..57f157ba8 100644 --- a/python/valuecell/server/db/models/__init__.py +++ b/python/valuecell/server/db/models/__init__.py @@ -1,5 +1,20 @@ -"""Database models for ValueCell Server.""" +""" +ValueCell Server - Database Models +This package contains all database models for the ValueCell server. +All models are automatically imported to ensure they are registered with SQLAlchemy. +""" + +# Import base model from .base import Base -__all__ = ["Base"] +# Import all models to ensure they are registered with SQLAlchemy +from .agent import Agent +from .asset import Asset + +# Export all models +__all__ = [ + "Base", + "Agent", + "Asset", +] diff --git a/python/valuecell/server/db/models/agent.py b/python/valuecell/server/db/models/agent.py new file mode 100644 index 000000000..b528e0eec --- /dev/null +++ b/python/valuecell/server/db/models/agent.py @@ -0,0 +1,124 @@ +""" +ValueCell Server - Agent Models + +This module defines the database models for agents in the ValueCell system. +""" + +from typing import Dict, Any +from sqlalchemy import Column, Integer, String, Text, Boolean, DateTime, JSON +from sqlalchemy.sql import func + +from .base import Base + + +class Agent(Base): + """ + Agent model representing an AI agent in the ValueCell system. + + This table stores information about available agents, their capabilities, + configuration, and metadata. + """ + + __tablename__ = "agents" + + # Primary key + id = Column(Integer, primary_key=True, index=True) + + # Basic agent information + name = Column( + String(100), + unique=True, + nullable=False, + index=True, + comment="Unique agent name/identifier", + ) + display_name = Column( + String(200), nullable=True, comment="Human-readable display name" + ) + description = Column( + Text, + nullable=True, + comment="Detailed description of the agent's purpose and capabilities", + ) + + # Agent configuration + version = Column( + String(50), nullable=True, default="1.0.0", comment="Agent version" + ) + + # Status and availability + enabled = Column( + Boolean, + default=True, + nullable=False, + comment="Whether the agent is currently enabled", + ) + is_active = Column( + Boolean, + default=True, + nullable=False, + comment="Whether the agent is active and available", + ) + + # Capabilities and metadata + capabilities = Column( + JSON, + nullable=True, + comment="JSON object describing agent capabilities (streaming, notifications, etc.)", + ) + agent_metadata = Column( + JSON, + nullable=True, + comment="Additional metadata (author, tags, supported features, etc.)", + ) + + # Configuration + config = Column( + JSON, nullable=True, comment="Agent-specific configuration parameters" + ) + + # Timestamps + 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, + ) + + def __repr__(self): + return f"" + + def to_dict(self) -> Dict[str, Any]: + """Convert agent to dictionary representation.""" + return { + "id": self.id, + "name": self.name, + "display_name": self.display_name, + "description": self.description, + "version": self.version, + "enabled": self.enabled, + "is_active": self.is_active, + "capabilities": self.capabilities, + "agent_metadata": self.agent_metadata, + "config": self.config, + "created_at": self.created_at.isoformat() if self.created_at else None, + "updated_at": self.updated_at.isoformat() if self.updated_at else None, + } + + @classmethod + def from_config(cls, config_data: Dict[str, Any]) -> "Agent": + """Create an Agent instance from configuration data.""" + return cls( + name=config_data.get("name"), + display_name=config_data.get("display_name", config_data.get("name")), + description=config_data.get("description"), + version=config_data.get("version", "1.0.0"), + enabled=config_data.get("enabled", True), + is_active=config_data.get("is_active", True), + capabilities=config_data.get("capabilities"), + agent_metadata=config_data.get("metadata"), + config=config_data.get("config"), + ) diff --git a/python/valuecell/server/db/models/asset.py b/python/valuecell/server/db/models/asset.py new file mode 100644 index 000000000..2b25926d6 --- /dev/null +++ b/python/valuecell/server/db/models/asset.py @@ -0,0 +1,126 @@ +""" +ValueCell Server - Asset Models + +This module defines the database models for assets in the ValueCell system. +""" + +from typing import Dict, Any +from sqlalchemy import Column, Integer, String, Text, Boolean, DateTime, JSON, Numeric +from sqlalchemy.sql import func + +from .base import Base + + +class Asset(Base): + """ + Asset model representing financial assets in the ValueCell system. + + This table stores information about financial assets including stocks, bonds, + cryptocurrencies, and other investment instruments. + """ + + __tablename__ = "assets" + + # Primary key + id = Column(Integer, primary_key=True, index=True) + + # Basic asset information + symbol = Column( + String(50), + unique=True, + nullable=False, + index=True, + comment="Asset symbol/ticker (e.g., AAPL, BTC, etc.)", + ) + name = Column(String(200), nullable=False, comment="Full name of the asset") + description = Column( + Text, + nullable=True, + comment="Detailed description of the asset", + ) + + # Asset classification + asset_type = Column( + String(50), + nullable=False, + index=True, + comment="Type of asset (stock, bond, crypto, commodity, etc.)", + ) + sector = Column( + String(100), + nullable=True, + comment="Industry sector (for stocks)", + ) + + # Market data + current_price = Column( + Numeric(precision=20, scale=8), + nullable=True, + comment="Current market price", + ) + + # Status and availability + is_active = Column( + Boolean, + default=True, + nullable=False, + comment="Whether the asset is active", + ) + + # Metadata and configuration + asset_metadata = Column( + JSON, + nullable=True, + comment="Additional metadata (ISIN, CUSIP, fundamental data, etc.)", + ) + config = Column( + JSON, + nullable=True, + comment="Asset-specific configuration parameters", + ) + + # Timestamps + 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, + ) + + def __repr__(self): + return f"" + + def to_dict(self) -> Dict[str, Any]: + """Convert asset to dictionary representation.""" + return { + "id": self.id, + "symbol": self.symbol, + "name": self.name, + "description": self.description, + "asset_type": self.asset_type, + "sector": self.sector, + "current_price": float(self.current_price) if self.current_price else None, + "is_active": self.is_active, + "metadata": self.asset_metadata, + "config": self.config, + "created_at": self.created_at.isoformat() if self.created_at else None, + "updated_at": self.updated_at.isoformat() if self.updated_at else None, + } + + @classmethod + def from_config(cls, config_data: Dict[str, Any]) -> "Asset": + """Create an Asset instance from configuration data.""" + return cls( + symbol=config_data.get("symbol"), + name=config_data.get("name"), + description=config_data.get("description"), + asset_type=config_data.get("asset_type", "stock"), + sector=config_data.get("sector"), + current_price=config_data.get("current_price"), + is_active=config_data.get("is_active", True), + asset_metadata=config_data.get("metadata"), + config=config_data.get("config"), + )