diff --git a/python/configs/agent_cards/sec_agent.json b/python/configs/agent_cards/sec_agent.json index ab42d52c7..0a6fb71c4 100644 --- a/python/configs/agent_cards/sec_agent.json +++ b/python/configs/agent_cards/sec_agent.json @@ -1,5 +1,5 @@ { - "name": "Sec13FundAgent", + "name": "SecAgent", "url": "http://localhost:10003/", "enabled": true } \ No newline at end of file diff --git a/python/scripts/init_database.py b/python/scripts/init_database.py deleted file mode 100644 index a38f25ced..000000000 --- a/python/scripts/init_database.py +++ /dev/null @@ -1,22 +0,0 @@ -#!/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/agents/sec_13F_agent.py b/python/valuecell/agents/sec_13F_agent.py deleted file mode 100644 index 276b79ccd..000000000 --- a/python/valuecell/agents/sec_13F_agent.py +++ /dev/null @@ -1,219 +0,0 @@ -import asyncio -import logging -import os - -from agno.agent import Agent, RunResponse, RunResponseEvent # noqa -from agno.models.openrouter import OpenRouter -from edgar import Company, set_identity -from pydantic import BaseModel, Field, field_validator - -from valuecell.core.types import BaseAgent -from valuecell.core.agent.decorator import create_wrapped_agent - -# Configure logging -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) - - -class Sec13FundRequest(BaseModel): - ticker: str = Field( - ..., - description="Stock ticker symbol to analyze (e.g., 'AAPL', 'TSLA'). Only one ticker symbol is allowed per request.", - ) - - @field_validator("ticker") - @classmethod - def validate_tickers(cls, v): - if not v or not isinstance(v, str): - raise ValueError("Ticker must be a non-empty string") - return v.upper().strip() - - -class Sec13FundAgentConfig: - """Configuration management class for SEC 13F Agent""" - - def __init__(self): - self.sec_email = os.getenv("SEC_EMAIL", "your.name@example.com") - self.parser_model_id = os.getenv("SEC_PARSER_MODEL_ID", "openai/gpt-4o-mini") - self.analysis_model_id = os.getenv( - "SEC_ANALYSIS_MODEL_ID", "deepseek/deepseek-chat-v3-0324" - ) - self.max_filings = int(os.getenv("SEC_MAX_FILINGS", "5")) - self.request_timeout = int(os.getenv("SEC_REQUEST_TIMEOUT", "30")) - - -# @serve(name="sec13fundAgent") -class Sec13FundAgent(BaseAgent): - """ - A simple agent that uses the SEC API to retrieve information about a company. - """ - - def __init__(self): - super().__init__() - self.config = Sec13FundAgentConfig() - - try: - # Agent for parsing queries - self.parser_agent = Agent( - model=OpenRouter(id=self.config.parser_model_id), - response_model=Sec13FundRequest, - markdown=True, - ) - # Agent for analysis - self.analysis_agent = Agent( - model=OpenRouter(id=self.config.analysis_model_id), - markdown=True, - ) - logger.info("SEC 13F Agent initialized successfully") - except Exception as e: - logger.error(f"Failed to initialize SEC 13F Agent: {e}") - raise - - async def stream(self, query: str, session_id: str, task_id: str): - """ - Main method for processing SEC 13F analysis requests - """ - try: - # 1. Parse query parameters - logger.info( - f"Processing SEC 13F request for session {session_id}, task {task_id}" - ) - - try: - run_response = self.parser_agent.run( - f"Parse the following sec 13 funds request and extract the parameters: {query}" - ) - sec_13_fund_request = run_response.content - company_name = sec_13_fund_request.ticker - logger.info(f"Parsed ticker: {company_name}") - except Exception as e: - logger.error(f"Failed to parse query: {e}") - yield { - "content": f"❌ **Parse Error**: Unable to parse query parameters. Please ensure you provide a valid stock ticker.\nError details: {str(e)}", - "is_task_complete": True, - } - return - - # 2. Set SEC identity and get company data - try: - set_identity(self.config.sec_email) - company = Company(company_name) - logger.info(f"Created company object for {company_name}") - except Exception as e: - logger.error(f"Failed to create company object: {e}") - yield { - "content": f"❌ **Company Query Error**: Unable to find company for ticker '{company_name}'.\nError details: {str(e)}", - "is_task_complete": True, - } - return - - # 3. Get 13F-HR filings - try: - filings = company.get_filings(form="13F-HR").head( - self.config.max_filings - ) - if len(filings) < 2: - yield { - "content": f"❌ **Insufficient Data**: Company '{company_name}' has insufficient 13F-HR filings (at least 2 filings required for comparison analysis).", - "is_task_complete": True, - } - return - logger.info(f"Retrieved {len(filings)} filings for {company_name}") - except Exception as e: - logger.error(f"Failed to get filings: {e}") - yield { - "content": f"❌ **Filing Retrieval Error**: Unable to retrieve 13F-HR filings for company '{company_name}'.\nError details: {str(e)}", - "is_task_complete": True, - } - return - - # 4. Parse filing data (fixed data order logic) - try: - # Get the latest filing (index 0 is the most recent) - current_filing = filings.iloc[0].obj() - current_data = current_filing.infotable.to_json() - - # Get the previous filing (index 1 is the previous period) - previous_filing = filings.iloc[1].obj() - previous_data = previous_filing.infotable.to_json() - - logger.info("Successfully parsed current and previous holdings data") - except Exception as e: - logger.error(f"Failed to parse filing data: {e}") - yield { - "content": f"❌ **Data Parsing Error**: Unable to parse 13F-HR filing data.\nError details: {str(e)}", - "is_task_complete": True, - } - return - - # 5. Generate analysis report - try: - analysis_prompt = f""" - As a professional investment analyst, please conduct an in-depth analysis of the following 13F holdings data: - - ## Previous Holdings Data (Earlier Period): - {previous_data} - - ## Current Holdings Data (Latest Period): - {current_data} - - ## Analysis Requirements: - Please provide professional analysis from the following perspectives: - - ### 1. Holdings Changes Summary - - New Positions: List newly acquired stocks and potential investment rationale - - Exits/Reductions: Analyze divested stocks and speculated reasons - - Position Adjustments: Focus on stocks with position changes exceeding 20% - - ### 2. Sector Allocation Analysis - - Sector Weight Changes: Adjustments in sector allocation percentages - - Investment Preferences: Changes in sector preferences reflected by capital flows - - Concentration Changes: Adjustments in portfolio concentration - - ### 3. Key Holdings Analysis - - Specific changes in top 10 holdings - - Calculate increase/decrease percentages for major positions - - Analyze possible reasons for significant position adjustments - - ### 4. Investment Strategy Insights - - Investment style adjustments reflected in holdings changes - - Market trend judgments and responses - - Changes in risk appetite - - ## Output Requirements: - Please output analysis results in a clear structure, including: - - 📊 **Key Findings**: 3-5 critical insights - - 📈 **Important Data**: Specific change data and percentages - - 🎯 **Investment Insights**: Reference value for investors - - ⚠️ **Risk Alerts**: Risk points that need attention - - Please ensure the analysis is objective and professional, based on factual data, avoiding excessive speculation. - """ - - result = self.analysis_agent.run(analysis_prompt) - logger.info("Analysis completed successfully") - - yield { - "content": result.content, - "is_task_complete": True, - } - - except Exception as e: - logger.error(f"Failed to generate analysis: {e}") - yield { - "content": f"❌ **Analysis Generation Error**: Unable to generate analysis report.\nError details: {str(e)}", - "is_task_complete": True, - } - return - - except Exception as e: - logger.error(f"Unexpected error in stream method: {e}") - yield { - "content": f"❌ **System Error**: An unexpected error occurred while processing the request.\nError details: {str(e)}", - "is_task_complete": True, - } - - -if __name__ == "__main__": - agent = create_wrapped_agent(Sec13FundAgent) - asyncio.run(agent.serve()) diff --git a/python/valuecell/agents/sec_agent.py b/python/valuecell/agents/sec_agent.py new file mode 100644 index 000000000..a25340ea7 --- /dev/null +++ b/python/valuecell/agents/sec_agent.py @@ -0,0 +1,387 @@ +import asyncio +import logging +import os +from enum import Enum + +from agno.agent import Agent, RunResponse, RunResponseEvent # noqa +from agno.models.openrouter import OpenRouter +from edgar import Company, set_identity +from pydantic import BaseModel, Field, field_validator + +from valuecell.core.types import BaseAgent +from valuecell.core.agent.decorator import create_wrapped_agent + +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +class QueryType(str, Enum): + """Query type enumeration""" + + FINANCIAL_DATA = "financial_data" # Financial data queries (10-K, 8-K, 10-Q) + FUND_HOLDINGS = "fund_holdings" # 13F fund holdings queries + + +class SecRequest(BaseModel): + """Unified SEC query request model""" + + ticker: str = Field( + ..., + description="Stock ticker symbol to analyze (e.g., 'AAPL', 'TSLA'). Only one ticker symbol is allowed per request.", + ) + query_type: QueryType = Field( + ..., + description="Type of SEC data to query: 'financial_data' for 10-K/8-K/10-Q filings, 'fund_holdings' for 13F holdings analysis", + ) + + @field_validator("ticker") + @classmethod + def validate_ticker(cls, v): + if not v or not isinstance(v, str): + raise ValueError("Ticker must be a non-empty string") + return v.upper().strip() + + +class Sec13FundRequest(BaseModel): + ticker: str = Field( + ..., + description="Stock ticker symbol to analyze (e.g., 'AAPL', 'TSLA'). Only one ticker symbol is allowed per request.", + ) + + @field_validator("ticker") + @classmethod + def validate_tickers(cls, v): + if not v or not isinstance(v, str): + raise ValueError("Ticker must be a non-empty string") + return v.upper().strip() + + +class Sec13FundAgentConfig: + """Configuration management class for SEC 13F Agent""" + + def __init__(self): + self.sec_email = os.getenv("SEC_EMAIL", "your.name@example.com") + self.parser_model_id = os.getenv("SEC_PARSER_MODEL_ID", "openai/gpt-4o-mini") + self.analysis_model_id = os.getenv( + "SEC_ANALYSIS_MODEL_ID", "deepseek/deepseek-chat-v3-0324" + ) + self.max_filings = int(os.getenv("SEC_MAX_FILINGS", "5")) + self.request_timeout = int(os.getenv("SEC_REQUEST_TIMEOUT", "30")) + + +class SecAgent(BaseAgent): + """ + Intelligent SEC analysis agent supporting financial data queries and 13F fund holdings analysis + """ + + def __init__(self): + super().__init__() + self.config = Sec13FundAgentConfig() + + try: + # Query classification agent - for determining query type + self.classifier_agent = Agent( + model=OpenRouter(id=self.config.parser_model_id), + response_model=SecRequest, + markdown=True, + ) + # Traditional 13F parsing agent - maintains backward compatibility + self.parser_agent = Agent( + model=OpenRouter(id=self.config.parser_model_id), + response_model=Sec13FundRequest, + markdown=True, + ) + # Analysis agent + self.analysis_agent = Agent( + model=OpenRouter(id=self.config.analysis_model_id), + markdown=True, + ) + logger.info("SEC intelligent analysis agent initialized successfully") + except Exception as e: + logger.error(f"Failed to initialize SEC Agent: {e}") + raise + + async def _classify_query(self, query: str) -> QueryType: + """ + Intelligently classify user queries to determine if it's financial data or 13F query + """ + classification_prompt = f""" + Please analyze the following user query and determine what type of SEC data the user wants to obtain: + + User query: "{query}" + + Please judge the user's intent based on the query content: + 1. If the user explicitly wants to understand company financial data, financial statements, annual reports, quarterly reports, major events, etc., choose "financial_data" + 2. If the user explicitly wants to understand 13F fund holdings, institutional investor holding changes, fund shareholding situations, etc., choose "fund_holdings" + + Keyword hints: + - Financial data related: financial statements, annual reports, quarterly reports, 10-K, 8-K, 10-Q, financial condition, revenue, profit, balance sheet, cash flow + - 13F holdings related: fund holdings, institutional investors, shareholding changes, 13F, fund shareholding, investment portfolio + + Please extract the stock ticker and determine the query type. + """ + + try: + response = self.classifier_agent.run(classification_prompt) + return response.content.query_type + except Exception as e: + logger.warning( + f"Query classification failed, defaulting to 13F analysis: {e}" + ) + # If classification fails, default to 13F analysis (maintains backward compatibility) + return QueryType.FUND_HOLDINGS + + async def _process_financial_data_query( + self, ticker: str, session_id: str, task_id: str + ): + """ + Process financial data queries (10-K, 8-K, 10-Q) + """ + try: + # Set SEC identity + set_identity(self.config.sec_email) + company = Company(ticker) + logger.info(f"Starting financial data query for {ticker}") + + # Get different types of financial filings + filing_types = ["10-K", "8-K", "10-Q", "4"] + all_filings_data = {} + + for filing_type in filing_types: + try: + filings = company.get_filings(form=filing_type).head(3) + if len(filings) > 0: + all_filings_data[filing_type] = [] + for i, filing in filings.iterrows(): + filing_info = { + "date": filing.filing_date, + "accession_number": filing.accession_number, + "form": filing.form, + } + all_filings_data[filing_type].append(filing_info) + logger.info(f"Retrieved {len(filings)} {filing_type} filings") + except Exception as e: + logger.warning(f"Failed to retrieve {filing_type} filings: {e}") + continue + + if not all_filings_data: + yield { + "content": f"❌ **Insufficient Data**: No financial filings found for company '{ticker}'.", + "is_task_complete": True, + } + return + + # Generate financial data analysis report + analysis_prompt = f""" + As a professional financial analyst, please analyze the following company's SEC financial filings: + + Company ticker: {ticker} + + ## Available financial filings: + {all_filings_data} + + ## Analysis requirements: + Please provide professional analysis from the following perspectives: + + ### 1. Financial Filing Overview + - Latest 10-K annual report status + - Latest 10-Q quarterly report status + - Important 8-K event disclosures + + ### 2. Filing Timeline Analysis + - Time distribution of filing documents + - Filing frequency and timeliness + + ### 3. Key Financial Events + - Major events identified from 8-K filings + - Important information that may affect investment decisions + + ### 4. Investment Recommendations + - Investment references based on filing documents + - Risk points that need attention + + ## Output requirements: + Please output analysis results in a clear structure, including: + - 📊 **Key Findings**: 3-5 important insights + - 📈 **Financial Highlights**: Important financial data and trends + - 🎯 **Investment Reference**: Reference value for investors + - ⚠️ **Risk Alerts**: Risk points that need attention + + Please ensure the analysis is objective and professional, based on actual data, avoiding excessive speculation. + """ + + result = self.analysis_agent.run(analysis_prompt) + logger.info("Financial data analysis completed") + + yield { + "content": result.content, + "is_task_complete": True, + } + + except Exception as e: + logger.error(f"Financial data query failed: {e}") + yield { + "content": f"❌ **Financial Data Query Error**: Unable to retrieve financial data for company '{ticker}'.\nError details: {str(e)}", + "is_task_complete": True, + } + + async def _process_fund_holdings_query( + self, ticker: str, session_id: str, task_id: str + ): + """ + Process 13F fund holdings queries (original logic) + """ + try: + # Set SEC identity + set_identity(self.config.sec_email) + company = Company(ticker) + logger.info(f"Starting 13F holdings data query for {ticker}") + + # Get 13F-HR filings + filings = company.get_filings(form="13F-HR").head(self.config.max_filings) + if len(filings) < 2: + yield { + "content": f"❌ **Insufficient Data**: Company '{ticker}' has insufficient 13F-HR filings (at least 2 filings required for comparison analysis).", + "is_task_complete": True, + } + return + logger.info(f"Retrieved {len(filings)} 13F filings") + + # Parse filing data + current_filing = filings.iloc[0].obj() + current_data = current_filing.infotable.to_json() + + previous_filing = filings.iloc[1].obj() + previous_data = previous_filing.infotable.to_json() + + logger.info("Successfully parsed current and historical holdings data") + + # Generate 13F analysis report + analysis_prompt = f""" + As a professional investment analyst, please conduct an in-depth analysis of the following 13F holdings data: + + ## Historical holdings data (earlier period): + {previous_data} + + ## Current holdings data (latest period): + {current_data} + + ## Analysis requirements: + Please provide professional analysis from the following perspectives: + + ### 1. Holdings Changes Summary + - New positions: List newly purchased stocks and possible investment rationale + - Exits/reductions: Analyze sold stocks and speculated reasons + - Position adjustments: Focus on stocks with position changes exceeding 20% + + ### 2. Sector Allocation Analysis + - Sector weight changes: Adjustments in sector allocation percentages + - Investment preferences: Changes in sector preferences reflected by capital flows + - Concentration changes: Adjustments in portfolio concentration + + ### 3. Key Holdings Analysis + - Specific changes in top 10 holdings + - Calculate increase/decrease percentages for major positions + - Analyze possible reasons for significant position adjustments + + ### 4. Investment Strategy Insights + - Investment style adjustments reflected in holdings changes + - Market trend judgments and responses + - Changes in risk appetite + + ## Output requirements: + Please output analysis results in a clear structure, including: + - 📊 **Key Findings**: 3-5 important insights + - 📈 **Important Data**: Specific change data and percentages + - 🎯 **Investment Insights**: Reference value for investors + - ⚠️ **Risk Alerts**: Risk points that need attention + + Please ensure the analysis is objective and professional, based on actual data, avoiding excessive speculation. + """ + + result = self.analysis_agent.run(analysis_prompt) + logger.info("13F analysis completed") + + yield { + "content": result.content, + "is_task_complete": True, + } + + except Exception as e: + logger.error(f"13F query failed: {e}") + yield { + "content": f"❌ **13F Query Error**: Unable to retrieve 13F data for company '{ticker}'.\nError details: {str(e)}", + "is_task_complete": True, + } + + async def stream(self, query: str, session_id: str, task_id: str): + """ + Main streaming method with intelligent routing support + """ + try: + logger.info( + f"Processing SEC query request - session: {session_id}, task: {task_id}" + ) + + # 1. Intelligent query classification + try: + query_type = await self._classify_query(query) + logger.info(f"Query classification result: {query_type}") + except Exception as e: + logger.error(f"Query classification failed: {e}") + yield { + "content": f"❌ **Classification Error**: Unable to analyze query type.\nError details: {str(e)}", + "is_task_complete": True, + } + return + + # 2. Extract stock ticker + try: + if query_type == QueryType.FINANCIAL_DATA: + # Use new classification agent to extract stock ticker + classification_prompt = f""" + Please extract the stock ticker from the following query: "{query}" + Please set query_type to "financial_data" and extract the ticker. + """ + response = self.classifier_agent.run(classification_prompt) + ticker = response.content.ticker + else: + # Use original parsing agent (maintains backward compatibility) + run_response = self.parser_agent.run( + f"Parse the following sec 13 funds request and extract the parameters: {query}" + ) + ticker = run_response.content.ticker + + logger.info(f"Extracted stock ticker: {ticker}") + except Exception as e: + logger.error(f"Stock ticker extraction failed: {e}") + yield { + "content": f"❌ **Parse Error**: Unable to parse query parameters. Please ensure you provide a valid stock ticker.\nError details: {str(e)}", + "is_task_complete": True, + } + return + + # 3. Route to appropriate processing method based on query type + if query_type == QueryType.FINANCIAL_DATA: + async for result in self._process_financial_data_query( + ticker, session_id, task_id + ): + yield result + else: # QueryType.FUND_HOLDINGS + async for result in self._process_fund_holdings_query( + ticker, session_id, task_id + ): + yield result + + except Exception as e: + logger.error(f"Unexpected error in stream method: {e}") + yield { + "content": f"❌ **System Error**: An unexpected error occurred while processing the request.\nError details: {str(e)}", + "is_task_complete": True, + } + + +if __name__ == "__main__": + agent = create_wrapped_agent(SecAgent) + asyncio.run(agent.serve()) diff --git a/python/valuecell/agents/sec_client.py b/python/valuecell/agents/sec_client.py index 0d96b7256..e11573891 100644 --- a/python/valuecell/agents/sec_client.py +++ b/python/valuecell/agents/sec_client.py @@ -11,11 +11,11 @@ async def main(): print(f"Available Agents: {available}") # Start Agent - calc_url = await connections.start_agent("Sec13FundAgent") + calc_url = await connections.start_agent("SecAgent") print(f"Calculator Agent started at: {calc_url}") # Get client and send message - client = await connections.get_client("Sec13FundAgent") + client = await connections.get_client("SecAgent") async for task, event in await client.send_message( "伯克希尔最近持仓变化", streaming=True ): diff --git a/python/valuecell/server/api/routers/websocket.py b/python/valuecell/server/api/routers/websocket.py index 52ab02965..67e1f6acf 100644 --- a/python/valuecell/server/api/routers/websocket.py +++ b/python/valuecell/server/api/routers/websocket.py @@ -14,7 +14,7 @@ logger = logging.getLogger(__name__) # Agent analyst mapping from the example -AGENT_ANALYST_MAP = {"Sec13FundAgent": ("Sec13FundAgent")} +AGENT_ANALYST_MAP = {"SecAgent": ("SecAgent")} class AnalysisRequest(BaseModel):