diff --git a/python/valuecell/server/api/app.py b/python/valuecell/server/api/app.py index a55a7f08e..169399fbd 100644 --- a/python/valuecell/server/api/app.py +++ b/python/valuecell/server/api/app.py @@ -17,6 +17,7 @@ from .routers.websocket import create_websocket_router from .routers.watchlist import create_watchlist_router from .routers.agent_stream import create_agent_stream_router +from .routers.agent import create_agent_router from .schemas import SuccessResponse, AppInfoData from ...adapters.assets import get_adapter_manager @@ -133,6 +134,8 @@ async def root(): app.include_router(create_watchlist_router()) # Include agent stream router app.include_router(create_agent_stream_router(), prefix="/api/v1") + # Include agent router + app.include_router(create_agent_router(), prefix="/api/v1") # For uvicorn diff --git a/python/valuecell/server/api/routers/__init__.py b/python/valuecell/server/api/routers/__init__.py index 05c6c1600..9c8f501fa 100644 --- a/python/valuecell/server/api/routers/__init__.py +++ b/python/valuecell/server/api/routers/__init__.py @@ -2,9 +2,11 @@ from .i18n import create_i18n_router, get_i18n_router from .system import create_system_router +from .agent import create_agent_router __all__ = [ "create_i18n_router", "get_i18n_router", "create_system_router", + "create_agent_router", ] diff --git a/python/valuecell/server/api/routers/agent.py b/python/valuecell/server/api/routers/agent.py new file mode 100644 index 000000000..ccea70226 --- /dev/null +++ b/python/valuecell/server/api/routers/agent.py @@ -0,0 +1,126 @@ +""" +Agent API router for handling agent-related endpoints. +""" + +from typing import Optional +from fastapi import APIRouter, Depends, HTTPException, Query, Path +from sqlalchemy.orm import Session + +from valuecell.server.db import get_db +from valuecell.server.services.agent_service import AgentService +from valuecell.server.api.schemas.agent import AgentListResponse, AgentResponse +from valuecell.server.api.schemas.base import SuccessResponse + + +def create_agent_router() -> APIRouter: + """Create and configure the agent router.""" + + router = APIRouter( + prefix="/agents", + tags=["agents"], + responses={404: {"description": "Not found"}}, + ) + + @router.get( + "/", + response_model=AgentListResponse, + summary="Get all agents", + description="Get a list of all agents in the system, including basic information", + ) + async def get_agents( + enabled_only: bool = Query(False, description="Return only enabled agents"), + name_filter: Optional[str] = Query( + None, description="Filter agents by name (supports fuzzy matching)" + ), + db: Session = Depends(get_db), + ) -> AgentListResponse: + """ + Get all agents list. + + - **enabled_only**: If True, return only enabled agents + - **name_filter**: Filter by agent name or display name with fuzzy matching + + Returns a response containing the agent list and statistics. + """ + try: + agent_list_data = AgentService.get_all_agents( + db=db, enabled_only=enabled_only, name_filter=name_filter + ) + return SuccessResponse.create( + data=agent_list_data, + msg=f"Successfully retrieved {agent_list_data.total} agents", + ) + except Exception as e: + raise HTTPException( + status_code=500, detail=f"Failed to retrieve agent list: {str(e)}" + ) + + @router.get( + "/{agent_id}", + response_model=AgentResponse, + summary="Get agent by ID", + description="Get detailed information of an agent by its ID", + ) + async def get_agent_by_id( + agent_id: int = Path(..., description="Unique identifier of the agent"), + db: Session = Depends(get_db), + ) -> AgentResponse: + """ + Get detailed information of a specific agent by ID. + + - **agent_id**: Unique identifier of the agent + + Returns detailed agent information, or 404 error if agent doesn't exist. + """ + try: + agent = AgentService.get_agent_by_id(db=db, agent_id=agent_id) + if not agent: + raise HTTPException( + status_code=404, detail=f"Agent with ID {agent_id} not found" + ) + return SuccessResponse.create( + data=agent, msg="Successfully retrieved agent information" + ) + except HTTPException: + raise + except Exception as e: + raise HTTPException( + status_code=500, + detail=f"Failed to retrieve agent information: {str(e)}", + ) + + @router.get( + "/by-name/{agent_name}", + response_model=AgentResponse, + summary="Get agent by name", + description="Get detailed information of an agent by its name", + ) + async def get_agent_by_name( + agent_name: str = Path(..., description="Name of the agent"), + db: Session = Depends(get_db), + ) -> AgentResponse: + """ + Get detailed information of a specific agent by name. + + - **agent_name**: Name of the agent + + Returns detailed agent information, or 404 error if agent doesn't exist. + """ + try: + agent = AgentService.get_agent_by_name(db=db, agent_name=agent_name) + if not agent: + raise HTTPException( + status_code=404, detail=f"Agent with name '{agent_name}' not found" + ) + return SuccessResponse.create( + data=agent, msg="Successfully retrieved agent information" + ) + except HTTPException: + raise + except Exception as e: + raise HTTPException( + status_code=500, + detail=f"Failed to retrieve agent information: {str(e)}", + ) + + return router diff --git a/python/valuecell/server/api/schemas/agent.py b/python/valuecell/server/api/schemas/agent.py new file mode 100644 index 000000000..86b4d8227 --- /dev/null +++ b/python/valuecell/server/api/schemas/agent.py @@ -0,0 +1,108 @@ +""" +Agent API schemas for handling agent-related requests and responses. +""" + +from typing import List, Optional, Dict, Any +from pydantic import BaseModel, Field +from datetime import datetime +from .base import SuccessResponse + + +class AgentCapabilities(BaseModel): + """Agent capabilities model.""" + + streaming: bool = Field(False, description="Whether the agent supports streaming") + push_notifications: bool = Field( + False, description="Whether the agent supports push notifications" + ) + + +class AgentMetadata(BaseModel): + """Agent metadata model.""" + + version: Optional[str] = Field(None, description="Agent version") + author: Optional[str] = Field(None, description="Agent author") + tags: Optional[List[str]] = Field(None, description="Agent tags") + + +class AgentData(BaseModel): + """Data model for a single agent.""" + + id: int = Field(..., description="Agent unique ID") + name: str = Field(..., description="Agent unique name/identifier") + display_name: Optional[str] = Field(None, description="Human-readable display name") + description: Optional[str] = Field(None, description="Agent description") + version: Optional[str] = Field(None, description="Agent version") + enabled: bool = Field(..., description="Whether the agent is enabled") + agent_metadata: Optional[Dict[str, Any]] = Field(None, description="Agent metadata") + config: Optional[Dict[str, Any]] = Field(None, description="Agent configuration") + created_at: Optional[datetime] = Field(None, description="Creation timestamp") + updated_at: Optional[datetime] = Field(None, description="Last update timestamp") + + class Config: + json_schema_extra = { + "example": { + "id": 1, + "name": "MarketAnalystAgent", + "display_name": "Market Analyst Agent", + "description": "AI-powered market analysis agent", + "version": "1.0.0", + "enabled": True, + "agent_metadata": { + "author": "ValueCell Team", + "tags": ["market", "analysis", "ai"], + }, + "config": {}, + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:00:00Z", + } + } + + +class AgentListData(BaseModel): + """Data model for agent list.""" + + agents: List[AgentData] = Field(..., description="List of agents") + total: int = Field(..., description="Total number of agents") + enabled_count: int = Field(..., description="Number of enabled agents") + + class Config: + json_schema_extra = { + "example": { + "agents": [ + { + "id": 1, + "name": "MarketAnalystAgent", + "display_name": "Market Analyst Agent", + "description": "AI-powered market analysis agent", + "version": "1.0.0", + "enabled": True, + "agent_metadata": { + "author": "ValueCell Team", + "tags": ["market", "analysis", "ai"], + }, + "config": {}, + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:00:00Z", + } + ], + "total": 1, + "enabled_count": 1, + } + } + + +class AgentQueryParams(BaseModel): + """Query parameters for agent list.""" + + enabled_only: Optional[bool] = Field( + False, description="Filter only enabled agents" + ) + name_filter: Optional[str] = Field( + None, description="Filter by agent name (partial match)" + ) + + +# Type aliases for SuccessResponse +AgentResponse = SuccessResponse[AgentData] +AgentListResponse = SuccessResponse[AgentListData] diff --git a/python/valuecell/server/db/init_db.py b/python/valuecell/server/db/init_db.py index 351b3bcf0..51acdac2a 100644 --- a/python/valuecell/server/db/init_db.py +++ b/python/valuecell/server/db/init_db.py @@ -201,7 +201,6 @@ def initialize_assets_with_service(self) -> bool: symbol=asset_ticker, name=asset_data["display_name"], asset_type=asset_data["asset_type"], - is_active=True, asset_metadata={ "exchange": asset_data.get("exchange") or ticker.split(":")[0], @@ -227,7 +226,6 @@ def initialize_assets_with_service(self) -> bool: # Update existing asset with adapter data existing_asset.name = asset_data["display_name"] existing_asset.asset_type = asset_data["asset_type"] - existing_asset.is_active = True # Update existing asset metadata existing_metadata = existing_asset.asset_metadata or {} existing_metadata.update( @@ -426,7 +424,6 @@ def initialize_basic_data(self) -> bool: "description": "AI-powered hedge fund analysis and trading agent", "version": "1.0.0", "enabled": True, - "is_active": True, "capabilities": { "streaming": False, "push_notifications": False, @@ -443,7 +440,6 @@ def initialize_basic_data(self) -> bool: "description": "SEC 13F fund analysis and tracking agent", "version": "1.0.0", "enabled": True, - "is_active": True, "capabilities": { "streaming": False, "push_notifications": False, @@ -460,7 +456,6 @@ def initialize_basic_data(self) -> bool: "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, @@ -531,9 +526,6 @@ def initialize_basic_data(self) -> bool: 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 ) diff --git a/python/valuecell/server/db/models/agent.py b/python/valuecell/server/db/models/agent.py index b528e0eec..1954b532b 100644 --- a/python/valuecell/server/db/models/agent.py +++ b/python/valuecell/server/db/models/agent.py @@ -53,12 +53,6 @@ class Agent(Base): 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( @@ -100,7 +94,6 @@ def to_dict(self) -> Dict[str, Any]: "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, @@ -113,11 +106,10 @@ 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")), + display_name=config_data.get("display_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/services/agent_service.py b/python/valuecell/server/services/agent_service.py new file mode 100644 index 000000000..08f52e277 --- /dev/null +++ b/python/valuecell/server/services/agent_service.py @@ -0,0 +1,134 @@ +""" +Agent service layer for handling agent-related business logic. +""" + +from typing import Optional +from sqlalchemy.orm import Session +from sqlalchemy import and_, or_ + +from valuecell.server.db.models.agent import Agent +from valuecell.server.api.schemas.agent import AgentData, AgentListData + + +class AgentService: + """Service class for agent-related operations.""" + + @staticmethod + def get_all_agents( + db: Session, enabled_only: bool = False, name_filter: Optional[str] = None + ) -> AgentListData: + """ + Get all agents from database with optional filters. + + Args: + db: Database session + enabled_only: Filter only enabled agents + name_filter: Filter by agent name (partial match) + + Returns: + AgentListData with agents list and statistics + """ + # Build query with filters + query = db.query(Agent) + + filters = [] + if enabled_only: + filters.append(Agent.enabled) + if name_filter: + filters.append( + or_( + Agent.name.ilike(f"%{name_filter}%"), + Agent.display_name.ilike(f"%{name_filter}%"), + ) + ) + + if filters: + query = query.filter(and_(*filters)) + + # Execute query + agents = query.order_by(Agent.created_at.desc()).all() + + # Convert to data models + agent_data_list = [ + AgentData( + id=agent.id, + name=agent.name, + display_name=agent.display_name, + description=agent.description, + version=agent.version, + enabled=agent.enabled, + agent_metadata=agent.agent_metadata, + config=agent.config, + created_at=agent.created_at, + updated_at=agent.updated_at, + ) + for agent in agents + ] + + # Calculate statistics + total_count = len(agent_data_list) + enabled_count = sum(1 for agent in agent_data_list if agent.enabled) + + return AgentListData( + agents=agent_data_list, total=total_count, enabled_count=enabled_count + ) + + @staticmethod + def get_agent_by_id(db: Session, agent_id: int) -> Optional[AgentData]: + """ + Get a specific agent by ID. + + Args: + db: Database session + agent_id: Agent ID + + Returns: + AgentData if found, None otherwise + """ + agent = db.query(Agent).filter(Agent.id == agent_id).first() + + if not agent: + return None + + return AgentData( + id=agent.id, + name=agent.name, + display_name=agent.display_name, + description=agent.description, + version=agent.version, + enabled=agent.enabled, + agent_metadata=agent.agent_metadata, + config=agent.config, + created_at=agent.created_at, + updated_at=agent.updated_at, + ) + + @staticmethod + def get_agent_by_name(db: Session, agent_name: str) -> Optional[AgentData]: + """ + Get a specific agent by name. + + Args: + db: Database session + agent_name: Agent name + + Returns: + AgentData if found, None otherwise + """ + agent = db.query(Agent).filter(Agent.name == agent_name).first() + + if not agent: + return None + + return AgentData( + id=agent.id, + name=agent.name, + display_name=agent.display_name, + description=agent.description, + version=agent.version, + enabled=agent.enabled, + agent_metadata=agent.agent_metadata, + config=agent.config, + created_at=agent.created_at, + updated_at=agent.updated_at, + )