diff --git a/requirements.txt b/requirements.txt index 0d993e3a..01e3e53c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -28,4 +28,5 @@ uvicorn[standard] pydantic python-multipart python-jose[cryptography] -passlib[bcrypt] \ No newline at end of file +passlib[bcrypt] +python-dotenv \ No newline at end of file diff --git a/tradingagents/graph/trading_graph.py b/tradingagents/graph/trading_graph.py index 14591349..2802e09b 100644 --- a/tradingagents/graph/trading_graph.py +++ b/tradingagents/graph/trading_graph.py @@ -190,11 +190,14 @@ class TradingAgentsGraph: # Log state self._log_state(trade_date, final_state) + eval_results_path=f"{RESULTS_BASE}" + input_file_path = f"{eval_results_path}/{company_name}/TradingAgentsStrategy_logs/full_states_log_{trade_date}.json" + output_file_path = f"{eval_results_path}/{company_name}/TradingAgentsStrategy_transformed_logs/" + # Transform output JSON into widget-friendly format - data_transformation_agent = DataTransformationAgent(TransformationConfig( - eval_results_path=f"{RESULTS_BASE}/{company_name}/TradingAgentsStrategy_transformed_logs/full_states_log_{trade_date}.json")) + data_transformation_agent = DataTransformationAgent(TransformationConfig(eval_results_path=eval_results_path)) - transformed_output = data_transformation_agent.transform_single_file(self._get_state(trade_date)) + transformed_output = data_transformation_agent.process_single_file(input_file_path, output_file_path) # Return decision and processed signal return transformed_output, self.process_signal(final_state["final_trade_decision"]) @@ -232,11 +235,12 @@ class TradingAgentsGraph: } # Save to file - directory = Path(f"../output_data/{self.ticker}/TradingAgentsStrategy_logs/") + output_directory_path = os.path.join(os.path.dirname(__file__), "..", "..", "output_data",f"{self.ticker}", "TradingAgentsStrategy_logs") + directory = Path(output_directory_path) directory.mkdir(parents=True, exist_ok=True) with open( - f"../output_data/{self.ticker}/TradingAgentsStrategy_logs/full_states_log_{trade_date}.json", + f"{output_directory_path}/full_states_log_{trade_date}.json", "w", ) as f: json.dump(self.log_states_dict, f, indent=4) diff --git a/web_app/backend/main.py b/web_app/backend/main.py index 9f3778b7..c16d39e9 100644 --- a/web_app/backend/main.py +++ b/web_app/backend/main.py @@ -7,6 +7,17 @@ import os from datetime import datetime import glob import uuid +from starlette.concurrency import run_in_threadpool + +# Load environment variables from .env (if present) +try: + from dotenv import load_dotenv, find_dotenv + _dotenv_path = find_dotenv() + if _dotenv_path: + load_dotenv(_dotenv_path) +except Exception: + # dotenv is optional; ignore if not installed + pass # Import your TradingAgents components import sys @@ -19,6 +30,13 @@ app = FastAPI(title="TradingAgents API", version="1.0.0", debug=True) # Centralized results directory to avoid repetition RESULTS_BASE = os.path.join(os.path.dirname(__file__), "..", "..", "output_data") +# Simple startup check for OPENAI_API_KEY +OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") +if not OPENAI_API_KEY: + print("[WARN] OPENAI_API_KEY is not set. Set it in your shell or in a .env file.") +else: + print("[INFO] OPENAI_API_KEY detected from environment.") + # Configure CORS app.add_middleware( CORSMiddleware, @@ -58,38 +76,37 @@ async def health_check(): return {"status": "healthy", "timestamp": datetime.now().isoformat()} async def run_analysis_task(job_id: str, symbol: str, analysis_date: str, config_overrides: Dict[str, Any] = None): - """Background task to run the trading analysis""" + """Background task to run the trading analysis without blocking the event loop""" try: jobs[job_id].status = "running" jobs[job_id].progress = "Initializing TradingAgents..." - - # Create custom config + + # Prepare config config = DEFAULT_CONFIG.copy() if config_overrides: config.update(config_overrides) - - # Initialize TradingAgents + jobs[job_id].progress = "Setting up trading graph..." - # Do not set API keys in code. Use environment variables or a secure secret manager. - ta = TradingAgentsGraph(debug=True, config=config) - - # Run the analysis - jobs[job_id].progress = f"Analyzing {symbol} for {analysis_date}..." - _, decision = ta.propagate(symbol, analysis_date) - - print(_) - print("Decision: ", decision) - + # Define blocking work as sync function + def _do_work(): + ta = TradingAgentsGraph(debug=True, config=config) + jobs[job_id].progress = f"Analyzing {symbol} for {analysis_date}..." + _, decision = ta.propagate(symbol, analysis_date) + return decision + + # Run blocking work in threadpool so the event loop stays responsive + decision = await run_in_threadpool(_do_work) + jobs[job_id].status = "completed" jobs[job_id].result = { "symbol": symbol, "date": analysis_date, "decision": decision, - "completed_at": datetime.now().isoformat() + "completed_at": datetime.now().isoformat(), } jobs[job_id].progress = "Analysis completed successfully" - + except Exception as e: jobs[job_id].status = "failed" jobs[job_id].error = str(e) @@ -320,6 +337,20 @@ async def get_specific_transformed_result(symbol: str, date: str): except Exception as e: raise HTTPException(status_code=500, detail=f"Error reading file: {str(e)}") +@app.get("/jobs") +async def get_jobs(): + """Get all jobs""" + job_lst = [] + for job_id, job in jobs.items(): + job_lst.append({ + "job_id": job_id, + "status": job.status, + "progress": job.progress, + "result": job.result, + "error": job.error + }) + return {"jobs": job_lst} + @app.get("/config") async def get_default_config(): """Get the default configuration""" @@ -327,4 +358,4 @@ async def get_default_config(): if __name__ == "__main__": import uvicorn - uvicorn.run(app, host="0.0.0.0", port=8000, reload=True) + uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/web_app/frontend/src/App.js b/web_app/frontend/src/App.js index e1f33e13..26277b18 100644 --- a/web_app/frontend/src/App.js +++ b/web_app/frontend/src/App.js @@ -13,6 +13,7 @@ function App() { const [showDetailModal, setShowDetailModal] = useState(false); const [showWidgetsView, setShowWidgetsView] = useState(false); const [showTransformedDataModal, setShowTransformedDataModal] = useState(false); + const [showJobsModal, setShowJobsModal] = useState(false); const [analysisForm, setAnalysisForm] = useState({ symbol: '', date: '' }); const [isRunningAnalysis, setIsRunningAnalysis] = useState(false); const [selectedCompany, setSelectedCompany] = useState(null); @@ -29,6 +30,11 @@ function App() { const [transformedCompanyFiles, setTransformedCompanyFiles] = useState([]); const [activeDetailTab, setActiveDetailTab] = useState(null); + // Job management state + const [jobs, setJobs] = useState([]); + const [jobsStats, setJobsStats] = useState({ total: 0, running: 0, completed: 0, failed: 0 }); + const [isLoadingJobs, setIsLoadingJobs] = useState(false); + // Fields to display as pretty cards in the Details modal const detailFields = useMemo(() => ([ 'market_report', @@ -106,6 +112,10 @@ function App() { setShowResultsModal(false); return; } + if (showJobsModal) { + setShowJobsModal(false); + return; + } if (showAnalysisModal) { setShowAnalysisModal(false); } @@ -113,7 +123,7 @@ function App() { }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); - }, [showDetailModal, showWidgetsView, showTransformedDataModal, showResultsModal, showAnalysisModal]); + }, [showDetailModal, showWidgetsView, showTransformedDataModal, showResultsModal, showJobsModal, showAnalysisModal]); useEffect(() => { checkBackendStatus(); @@ -140,6 +150,10 @@ function App() { setShowResultsModal(false); return; } + if (showJobsModal) { + setShowJobsModal(false); + return; + } if (showAnalysisModal) { setShowAnalysisModal(false); } @@ -147,7 +161,7 @@ function App() { }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); - }, [showDetailModal, showWidgetsView, showTransformedDataModal, showResultsModal, showAnalysisModal]); + }, [showDetailModal, showWidgetsView, showTransformedDataModal, showResultsModal, showJobsModal, showAnalysisModal]); // Keep the active details tab in sync with available fields for the selected result useEffect(() => { @@ -314,6 +328,7 @@ function App() { setShowDetailModal(false); setShowWidgetsView(false); setShowTransformedDataModal(false); + setShowJobsModal(false); setSelectedResult(null); setResultDetail(null); setSelectedTransformedData(null); @@ -331,6 +346,32 @@ function App() { } }; + const fetchJobs = async () => { + setIsLoadingJobs(true); + try { + const response = await axios.get('/jobs'); + const jobsData = response.data.jobs || []; + setJobs(jobsData); + const stats = { + total: jobsData.length, + running: jobsData.filter((job) => job.status === 'running').length, + completed: jobsData.filter((job) => job.status === 'completed').length, + failed: jobsData.filter((job) => job.status === 'failed').length, + }; + setJobsStats(stats); + } catch (error) { + console.error('Error fetching jobs:', error); + } finally { + setIsLoadingJobs(false); + } + }; + + const handleViewJobResult = (job) => { + setSelectedResult(job); + setShowJobsModal(false); + setShowResultsModal(true); + }; + return (