diff --git a/python/scripts/launch.py b/python/scripts/launch.py new file mode 100644 index 000000000..6948780ff --- /dev/null +++ b/python/scripts/launch.py @@ -0,0 +1,128 @@ +""" +Interactive agent launcher script. +Allows users to select an agent from available options and launch it using uv. +""" + +import os +from pathlib import Path +import subprocess +from datetime import datetime +from typing import Dict + +import questionary + +# Mapping from agent name to analyst key (for ai-hedge-fund agents) +MAP_NAME_ANALYST: Dict[str, str] = { + "AswathDamodaranAgent": "aswath_damodaran", + "BenGrahamAgent": "ben_graham", + "BillAckmanAgent": "bill_ackman", + "CathieWoodAgent": "cathie_wood", + "CharlieMungerAgent": "charlie_munger", + "FundamentalsAnalystAgent": "fundamentals_analyst", + "MichaelBurryAgent": "michael_burry", + "MohnishPabraiAgent": "mohnish_pabrai", + "PeterLynchAgent": "peter_lynch", + "PhilFisherAgent": "phil_fisher", + "RakeshJhunjhunwalaAgent": "rakesh_jhunjhunwala", + "SentimentAnalystAgent": "sentiment_analyst", + "StanleyDruckenmillerAgent": "stanley_druckenmiller", + "TechnicalAnalystAgent": "technical_analyst", + "ValuationAnalystAgent": "valuation_analyst", + "WarrenBuffettAgent": "warren_buffett", +} +SEC_AGENT_NAME = "SecAgent" +TRADING_AGENTS_NAME = "TradingAgentsAdapter" +AGENTS = list(MAP_NAME_ANALYST.keys()) + [SEC_AGENT_NAME, TRADING_AGENTS_NAME] + +PROJECT_DIR = Path(__file__).resolve().parent.parent.parent +PYTHON_DIR = PROJECT_DIR / "python" +ENV_PATH = PROJECT_DIR / ".env" +ENV_PATH_STR = str(ENV_PATH.resolve()) + +# Mapping from agent name to launch command +MAP_NAME_COMMAND: Dict[str, str] = {} +for name, analyst in MAP_NAME_ANALYST.items(): + MAP_NAME_COMMAND[name] = ( + f"cd {PYTHON_DIR}/third_party/ai-hedge-fund && uv run --env-file {ENV_PATH} -m adapter --analyst {analyst}" + ) +MAP_NAME_COMMAND[SEC_AGENT_NAME] = ( + f"uv run --env-file {ENV_PATH} -m valuecell.agents.sec_agent" +) +MAP_NAME_COMMAND[TRADING_AGENTS_NAME] = ( + f"cd {PYTHON_DIR}/third_party/TradingAgents && uv run --env-file {ENV_PATH} -m adapter" +) +BACKEND_COMMAND = ( + f"cd {PYTHON_DIR} && uv run --env-file {ENV_PATH} -m valuecell.server.main" +) +FRONTEND_URL = "http://localhost:1420" + + +def check_envfile_is_set(): + if not ENV_PATH.exists(): + print( + f".env file not found at {ENV_PATH}. Please create it with necessary environment variables. " + "check python/.env.example for reference." + ) + exit(1) + + +def main(): + check_envfile_is_set() + timestamp = datetime.now().strftime("%Y%m%d%H%M%S") + log_dir = f"{PROJECT_DIR}/logs/{timestamp}" + + # Use questionary multi-select to allow choosing multiple agents + selected_agents = questionary.checkbox( + "Choose agents to launch (use space to select, enter to confirm):", + choices=AGENTS, + ).ask() + + if not selected_agents: + print("No agents selected.") + exit(1) + + os.makedirs(log_dir, exist_ok=True) + print(f"Logs will be saved to {log_dir}/") + + processes = [] + logfiles = [] + for selected_agent in selected_agents: + logfile_path = f"{log_dir}/{selected_agent}.log" + print(f"Starting agent: {selected_agent} - output to {logfile_path}") + + # Open logfile for writing + logfile = open(logfile_path, "w") + logfiles.append(logfile) + + # Launch command using Popen with output redirected to logfile + process = subprocess.Popen( + MAP_NAME_COMMAND[selected_agent], shell=True, stdout=logfile, stderr=logfile + ) + processes.append(process) + print("All agents launched. Waiting for tasks...") + + for selected_agent in selected_agents: + print( + f"You can monitor {selected_agent} logs at {log_dir}/{selected_agent}.log or chat on: {FRONTEND_URL}/agent/{selected_agent}" + ) + + # Launch backend + logfile_path = f"{log_dir}/backend.log" + print(f"Starting backend - output to {logfile_path}") + print(f"Frontend available at {FRONTEND_URL}") + logfile = open(logfile_path, "w") + logfiles.append(logfile) + process = subprocess.Popen( + BACKEND_COMMAND, shell=True, stdout=logfile, stderr=logfile + ) + processes.append(process) + + for process in processes: + process.wait() + for logfile in logfiles: + logfile.close() + print(f"All agents finished. Check {log_dir}/ for output.") + + +if __name__ == "__main__": + main() diff --git a/python/scripts/prepare_envs.sh b/python/scripts/prepare_envs.sh new file mode 100644 index 000000000..9f58e8358 --- /dev/null +++ b/python/scripts/prepare_envs.sh @@ -0,0 +1,87 @@ +#!/bin/bash + +# Color codes for output highlighting +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Function to print highlighted command +highlight_command() { + echo -e "${BLUE}Running: $1${NC}" +} + +# Check current directory and switch to python if needed +if [ -d "python" ] && [ -f "python/pyproject.toml" ] && [ -f ".gitignore" ]; then + echo -e "${YELLOW}Detected project root. Switching to python directory...${NC}" + cd python +elif [ ! -f "pyproject.toml" ] || [ ! -d "third_party" ]; then + echo -e "${RED}Error: This script must be run from the project python directory or project root. You are in $(pwd)${NC}" + exit 1 +fi + +# Final check if in python directory +if [ ! -f "pyproject.toml" ] || [ ! -d "third_party" ]; then + echo -e "${RED}Error: Failed to switch to python directory. You are in $(pwd)${NC}" + exit 1 +fi + +# Check if uv is installed +if ! command -v uv &> /dev/null; then + echo -e "${RED}Error: 'uv' command not found. Please install 'uv' (e.g., brew install uv).${NC}" + exit 1 +fi + +echo -e "${BLUE}==========================================${NC}" +echo -e "${BLUE}Starting environment preparation...${NC}" +echo -e "${BLUE}==========================================${NC}" + +# Prepare environments +echo -e "${GREEN}Project root confirmed. Preparing environments...${NC}" + +echo -e "${YELLOW}Setting up main Python environment...${NC}" +if [ ! -d ".venv" ]; then + highlight_command "uv venv --python 3.12" + uv venv --python 3.12 +else + echo -e "${YELLOW}.venv already exists, skipping venv creation.${NC}" +fi +highlight_command "uv sync --group dev" +uv sync --group dev +echo -e "${GREEN}Main environment setup complete.${NC}" + +echo -e "${BLUE}==========================================${NC}" +echo -e "${BLUE}Setting up third-party environments...${NC}" +echo -e "${BLUE}==========================================${NC}" +echo -e "${YELLOW}Setting up ai-hedge-fund environment...${NC}" +pushd ./third_party/ai-hedge-fund +if [ ! -d ".venv" ]; then + highlight_command "uv venv --python 3.12" + uv venv --python 3.12 +else + echo -e "${YELLOW}.venv already exists, skipping venv creation.${NC}" +fi +highlight_command "uv sync" +uv sync +popd +echo -e "${GREEN}ai-hedge-fund environment setup complete.${NC}" + +echo -e "${YELLOW}------------------------------------------${NC}" +echo -e "${YELLOW}Setting up TradingAgents environment...${NC}" +echo -e "${YELLOW}------------------------------------------${NC}" +pushd ./third_party/TradingAgents +if [ ! -d ".venv" ]; then + highlight_command "uv venv --python 3.12" + uv venv --python 3.12 +else + echo -e "${YELLOW}.venv already exists, skipping venv creation.${NC}" +fi +highlight_command "uv sync" +uv sync +popd +echo -e "${GREEN}TradingAgents environment setup complete.${NC}" + +echo -e "${GREEN}==========================================${NC}" +echo -e "${GREEN}All environments are set up.${NC}" +echo -e "${GREEN}==========================================${NC}" \ No newline at end of file diff --git a/python/third_party/TradingAgents/uv.lock b/python/third_party/TradingAgents/uv.lock index bc2aebd29..56407665c 100644 --- a/python/third_party/TradingAgents/uv.lock +++ b/python/third_party/TradingAgents/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.12" resolution-markers = [ "python_full_version >= '3.13'", @@ -140,6 +140,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, ] +[[package]] +name = "aiosqlite" +version = "0.21.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/13/7d/8bca2bf9a247c2c5dfeec1d7a5f40db6518f88d314b8bca9da29670d2671/aiosqlite-0.21.0.tar.gz", hash = "sha256:131bb8056daa3bc875608c631c678cda73922a2d4ba8aec373b19f18c17e7aa3", size = 13454, upload-time = "2025-02-03T07:30:16.235Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/10/6c25ed6de94c49f88a91fa5018cb4c0f3625f31d5be9f771ebe5cc7cd506/aiosqlite-0.21.0-py3-none-any.whl", hash = "sha256:2549cf4057f95f53dcba16f2b64e8e2791d7e1adedb13197dd8ed77bb226d7d0", size = 15792, upload-time = "2025-02-03T07:30:13.6Z" }, +] + [[package]] name = "akracer" version = "0.0.14" @@ -4764,6 +4776,7 @@ source = { editable = "../../" } dependencies = [ { name = "a2a-sdk", extra = ["http-server"] }, { name = "agno", extra = ["openai"] }, + { name = "aiosqlite" }, { name = "akshare" }, { name = "edgartools" }, { name = "fastapi" }, @@ -4781,7 +4794,9 @@ dependencies = [ requires-dist = [ { name = "a2a-sdk", extras = ["http-server"], specifier = ">=0.3.4" }, { name = "agno", extras = ["openai"], specifier = ">=1.8.2,<2.0" }, + { name = "aiosqlite", specifier = ">=0.19.0" }, { name = "akshare", specifier = ">=1.17.44" }, + { name = "diff-cover", marker = "extra == 'dev'", specifier = ">=9.0.0" }, { name = "edgartools", specifier = ">=4.12.2" }, { name = "fastapi", specifier = ">=0.104.0" }, { name = "pydantic", specifier = ">=2.0.0" }, @@ -4801,13 +4816,20 @@ provides-extras = ["dev"] [package.metadata.requires-dev] dev = [ + { name = "diff-cover", specifier = ">=9.0.0" }, + { name = "isort" }, { name = "pytest", specifier = ">=7.4.0" }, { name = "pytest-asyncio", specifier = ">=1.0.0" }, { name = "pytest-cov", specifier = ">=4.1.0" }, { name = "ruff" }, ] lint = [{ name = "ruff" }] +style = [ + { name = "isort" }, + { name = "ruff" }, +] test = [ + { name = "diff-cover", specifier = ">=9.0.0" }, { name = "pytest", specifier = ">=7.4.0" }, { name = "pytest-asyncio", specifier = ">=1.0.0" }, { name = "pytest-cov", specifier = ">=4.1.0" }, diff --git a/python/third_party/ai-hedge-fund/uv.lock b/python/third_party/ai-hedge-fund/uv.lock index 3d8de0abe..a2122d0cc 100644 --- a/python/third_party/ai-hedge-fund/uv.lock +++ b/python/third_party/ai-hedge-fund/uv.lock @@ -3115,6 +3115,7 @@ requires-dist = [ { name = "agno", extras = ["openai"], specifier = ">=1.8.2,<2.0" }, { name = "aiosqlite", specifier = ">=0.19.0" }, { name = "akshare", specifier = ">=1.17.44" }, + { name = "diff-cover", marker = "extra == 'dev'", specifier = ">=9.0.0" }, { name = "edgartools", specifier = ">=4.12.2" }, { name = "fastapi", specifier = ">=0.104.0" }, { name = "pydantic", specifier = ">=2.0.0" }, @@ -3134,13 +3135,20 @@ provides-extras = ["dev"] [package.metadata.requires-dev] dev = [ + { name = "diff-cover", specifier = ">=9.0.0" }, + { name = "isort" }, { name = "pytest", specifier = ">=7.4.0" }, { name = "pytest-asyncio", specifier = ">=1.0.0" }, { name = "pytest-cov", specifier = ">=4.1.0" }, { name = "ruff" }, ] lint = [{ name = "ruff" }] +style = [ + { name = "isort" }, + { name = "ruff" }, +] test = [ + { name = "diff-cover", specifier = ">=9.0.0" }, { name = "pytest", specifier = ">=7.4.0" }, { name = "pytest-asyncio", specifier = ">=1.0.0" }, { name = "pytest-cov", specifier = ">=4.1.0" }, diff --git a/python/valuecell/agents/__init__.py b/python/valuecell/agents/__init__.py index 2be9c8022..e69de29bb 100644 --- a/python/valuecell/agents/__init__.py +++ b/python/valuecell/agents/__init__.py @@ -1,61 +0,0 @@ -""" -Auto-import all agents to ensure they are registered with the AgentRegistry. -This module dynamically discovers and imports all agent classes. -""" - -import importlib -import inspect -import pkgutil -from pathlib import Path -from typing import List - -from valuecell.core.types import BaseAgent - - -def _discover_and_import_agents() -> List[str]: - """ - Dynamically discover and import all agent modules in this package. - - Returns: - List of agent class names that were imported - """ - imported_agents = [] - current_package = __name__ - current_path = Path(__file__).parent - - # Iterate through all Python files in the current directory - for _, module_name, _ in pkgutil.iter_modules([str(current_path)]): - if module_name.startswith("_"): - # Skip private modules - continue - - try: - # Import the module - module = importlib.import_module(f"{current_package}.{module_name}") - - # Find all classes in the module that inherit from BaseAgent - for name, obj in inspect.getmembers(module, inspect.isclass): - if ( - obj.__module__ == module.__name__ - and issubclass(obj, BaseAgent) - and obj != BaseAgent - ): - imported_agents.append(name) - # Make the class available at package level - globals()[name] = obj - - except Exception as e: - # Log import errors but continue with other modules - import logging - - logger = logging.getLogger(__name__) - logger.warning(f"Failed to import module {module_name}: {e}") - - return imported_agents - - -# Auto-import all agents -_imported_agent_names = _discover_and_import_agents() - -# Export all discovered agents for convenient access -__all__ = _imported_agent_names diff --git a/python/valuecell/core/agent/client.py b/python/valuecell/core/agent/client.py index 6e93f5981..d333834eb 100644 --- a/python/valuecell/core/agent/client.py +++ b/python/valuecell/core/agent/client.py @@ -60,7 +60,13 @@ async def _setup_client(self): client_factory = ClientFactory(config) card_resolver = A2ACardResolver(self._httpx_client, self.agent_url) - self.agent_card = await card_resolver.get_agent_card() + try: + self.agent_card = await card_resolver.get_agent_card() + except Exception as e: + raise RuntimeError( + "Failed to resolve agent card. Maybe the agent URL is incorrect or the agent is unreachable." + " Agents could be launched via `scripts/launch_agent.py`." + ) from e self._client = client_factory.create(self.agent_card) async def send_message( diff --git a/python/valuecell/core/agent/tests/test_client.py b/python/valuecell/core/agent/tests/test_client.py index 22a5b3617..6a79ce5de 100644 --- a/python/valuecell/core/agent/tests/test_client.py +++ b/python/valuecell/core/agent/tests/test_client.py @@ -262,3 +262,22 @@ async def test_close_closes_httpx_and_resets_state(self): assert client._httpx_client is None assert client._client is None assert client._initialized is False + + @pytest.mark.asyncio + async def test_ensure_initialized_card_resolution_failure(self): + """Test that ensure_initialized raises RuntimeError with helpful message on card resolution failure.""" + client = AgentClient("http://invalid-url.com") + + with patch('valuecell.core.agent.client.A2ACardResolver') as mock_resolver_class, \ + patch('httpx.AsyncClient'): + + mock_resolver = mock_resolver_class.return_value + mock_resolver.get_agent_card = AsyncMock(side_effect=Exception("Connection timeout")) + + with pytest.raises(RuntimeError) as exc_info: + await client.ensure_initialized() + + error_message = str(exc_info.value) + assert "Failed to resolve agent card" in error_message + assert "scripts/launch_agent.py" in error_message + assert "Connection timeout" in str(exc_info.value.__cause__) # Original exception should be chained diff --git a/start.sh b/start.sh index c6c8489bc..5618160fd 100755 --- a/start.sh +++ b/start.sh @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/bin/bash set -Eeuo pipefail # Simple project launcher with auto-install for bun and uv @@ -76,11 +76,11 @@ ensure_tool() { fi } -install_dependencies() { +compile() { # Backend deps if [[ -d "$PY_DIR" ]]; then info "Sync Python dependencies (uv sync)..." - (cd "$PY_DIR" && uv sync) + (cd "$PY_DIR" && bash scripts/prepare_envs.sh && uv run valuecell/server/db/init_db.py) success "Python dependencies synced" else warn "Backend directory not found: $PY_DIR. Skipping" @@ -91,7 +91,7 @@ install_dependencies() { info "Install frontend dependencies (bun install)..." (cd "$FRONTEND_DIR" && bun install) success "Frontend dependencies installed" - } else { + else warn "Frontend directory not found: $FRONTEND_DIR. Skipping" fi } @@ -101,11 +101,8 @@ start_backend() { warn "Backend directory not found; skipping backend start" return 0 fi - info "Starting backend (uv run python -m valuecell.server.main)..." - ( - cd "$PY_DIR" && uv run python -m valuecell.server.main - ) & BACKEND_PID=$! - info "Backend PID: $BACKEND_PID" + info "Starting backend (uv run scripts/launch.py)..." + cd "$PY_DIR" && uv run --with questionary scripts/launch.py } start_frontend() { @@ -166,16 +163,17 @@ main() { ensure_tool bun oven-sh/bun/bun ensure_tool uv uv - install_dependencies + compile - if (( start_backend_flag )); then - start_backend - fi if (( start_frontend_flag )); then start_frontend fi + sleep 5 # Give frontend a moment to start + + if (( start_backend_flag )); then + start_backend + fi - info "Services started. Press Ctrl+C to stop." # Wait for background jobs wait }