From 9a292cde34dd93b3c1314ffd6f1ce1ffea20abf6 Mon Sep 17 00:00:00 2001 From: hemangjoshi37a Date: Sun, 1 Feb 2026 06:55:15 +1100 Subject: [PATCH] ok --- frontend/backend/database.py | 467 +++++++++++++++++ frontend/backend/recommendations.db | Bin 40960 -> 323584 bytes frontend/backend/server.py | 286 ++++++++++- frontend/package.json | 1 + .../components/pipeline/AgentReportCard.tsx | 194 +++++++ .../components/pipeline/DataSourcesPanel.tsx | 185 +++++++ .../src/components/pipeline/DebateViewer.tsx | 254 +++++++++ .../components/pipeline/PipelineOverview.tsx | 157 ++++++ .../components/pipeline/RiskDebateViewer.tsx | 256 ++++++++++ frontend/src/components/pipeline/index.ts | 5 + frontend/src/pages/StockDetail.tsx | 481 +++++++++++++++--- frontend/src/services/api.ts | 163 +++++- frontend/src/types/pipeline.ts | 199 ++++++++ 13 files changed, 2560 insertions(+), 88 deletions(-) create mode 100644 frontend/src/components/pipeline/AgentReportCard.tsx create mode 100644 frontend/src/components/pipeline/DataSourcesPanel.tsx create mode 100644 frontend/src/components/pipeline/DebateViewer.tsx create mode 100644 frontend/src/components/pipeline/PipelineOverview.tsx create mode 100644 frontend/src/components/pipeline/RiskDebateViewer.tsx create mode 100644 frontend/src/components/pipeline/index.ts create mode 100644 frontend/src/types/pipeline.ts diff --git a/frontend/backend/database.py b/frontend/backend/database.py index 1353800c..325e1678 100644 --- a/frontend/backend/database.py +++ b/frontend/backend/database.py @@ -59,6 +59,85 @@ def init_db(): CREATE INDEX IF NOT EXISTS idx_stock_analysis_symbol ON stock_analysis(symbol) """) + # Create agent_reports table (stores each analyst's detailed report) + cursor.execute(""" + CREATE TABLE IF NOT EXISTS agent_reports ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + date TEXT NOT NULL, + symbol TEXT NOT NULL, + agent_type TEXT NOT NULL, + report_content TEXT, + data_sources_used TEXT, + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + UNIQUE(date, symbol, agent_type) + ) + """) + + # Create debate_history table (stores investment and risk debates) + cursor.execute(""" + CREATE TABLE IF NOT EXISTS debate_history ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + date TEXT NOT NULL, + symbol TEXT NOT NULL, + debate_type TEXT NOT NULL, + bull_arguments TEXT, + bear_arguments TEXT, + risky_arguments TEXT, + safe_arguments TEXT, + neutral_arguments TEXT, + judge_decision TEXT, + full_history TEXT, + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + UNIQUE(date, symbol, debate_type) + ) + """) + + # Create pipeline_steps table (stores step-by-step execution log) + cursor.execute(""" + CREATE TABLE IF NOT EXISTS pipeline_steps ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + date TEXT NOT NULL, + symbol TEXT NOT NULL, + step_number INTEGER, + step_name TEXT, + status TEXT, + started_at TEXT, + completed_at TEXT, + duration_ms INTEGER, + output_summary TEXT, + UNIQUE(date, symbol, step_number) + ) + """) + + # Create data_source_logs table (stores what raw data was fetched) + cursor.execute(""" + CREATE TABLE IF NOT EXISTS data_source_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + date TEXT NOT NULL, + symbol TEXT NOT NULL, + source_type TEXT, + source_name TEXT, + data_fetched TEXT, + fetch_timestamp TEXT, + success INTEGER DEFAULT 1, + error_message TEXT + ) + """) + + # Create indexes for new tables + cursor.execute(""" + CREATE INDEX IF NOT EXISTS idx_agent_reports_date_symbol ON agent_reports(date, symbol) + """) + cursor.execute(""" + CREATE INDEX IF NOT EXISTS idx_debate_history_date_symbol ON debate_history(date, symbol) + """) + cursor.execute(""" + CREATE INDEX IF NOT EXISTS idx_pipeline_steps_date_symbol ON pipeline_steps(date, symbol) + """) + cursor.execute(""" + CREATE INDEX IF NOT EXISTS idx_data_source_logs_date_symbol ON data_source_logs(date, symbol) + """) + conn.commit() conn.close() @@ -219,5 +298,393 @@ def get_all_recommendations() -> list: return [get_recommendation_by_date(date) for date in dates] +# ============== Pipeline Data Functions ============== + +def save_agent_report(date: str, symbol: str, agent_type: str, + report_content: str, data_sources_used: list = None): + """Save an individual agent's report.""" + conn = get_connection() + cursor = conn.cursor() + + try: + cursor.execute(""" + INSERT OR REPLACE INTO agent_reports + (date, symbol, agent_type, report_content, data_sources_used) + VALUES (?, ?, ?, ?, ?) + """, ( + date, symbol, agent_type, report_content, + json.dumps(data_sources_used) if data_sources_used else '[]' + )) + conn.commit() + finally: + conn.close() + + +def save_agent_reports_bulk(date: str, symbol: str, reports: dict): + """Save all agent reports for a stock at once. + + Args: + date: Date string (YYYY-MM-DD) + symbol: Stock symbol + reports: Dict with keys 'market', 'news', 'social_media', 'fundamentals' + """ + conn = get_connection() + cursor = conn.cursor() + + try: + for agent_type, report_data in reports.items(): + if isinstance(report_data, str): + report_content = report_data + data_sources = [] + else: + report_content = report_data.get('content', '') + data_sources = report_data.get('data_sources', []) + + cursor.execute(""" + INSERT OR REPLACE INTO agent_reports + (date, symbol, agent_type, report_content, data_sources_used) + VALUES (?, ?, ?, ?, ?) + """, (date, symbol, agent_type, report_content, json.dumps(data_sources))) + + conn.commit() + finally: + conn.close() + + +def get_agent_reports(date: str, symbol: str) -> dict: + """Get all agent reports for a stock on a date.""" + conn = get_connection() + cursor = conn.cursor() + + try: + cursor.execute(""" + SELECT agent_type, report_content, data_sources_used, created_at + FROM agent_reports + WHERE date = ? AND symbol = ? + """, (date, symbol)) + + reports = {} + for row in cursor.fetchall(): + reports[row['agent_type']] = { + 'agent_type': row['agent_type'], + 'report_content': row['report_content'], + 'data_sources_used': json.loads(row['data_sources_used']) if row['data_sources_used'] else [], + 'created_at': row['created_at'] + } + return reports + finally: + conn.close() + + +def save_debate_history(date: str, symbol: str, debate_type: str, + bull_arguments: str = None, bear_arguments: str = None, + risky_arguments: str = None, safe_arguments: str = None, + neutral_arguments: str = None, judge_decision: str = None, + full_history: str = None): + """Save debate history for investment or risk debate.""" + conn = get_connection() + cursor = conn.cursor() + + try: + cursor.execute(""" + INSERT OR REPLACE INTO debate_history + (date, symbol, debate_type, bull_arguments, bear_arguments, + risky_arguments, safe_arguments, neutral_arguments, + judge_decision, full_history) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, ( + date, symbol, debate_type, + bull_arguments, bear_arguments, + risky_arguments, safe_arguments, neutral_arguments, + judge_decision, full_history + )) + conn.commit() + finally: + conn.close() + + +def get_debate_history(date: str, symbol: str) -> dict: + """Get all debate history for a stock on a date.""" + conn = get_connection() + cursor = conn.cursor() + + try: + cursor.execute(""" + SELECT * FROM debate_history + WHERE date = ? AND symbol = ? + """, (date, symbol)) + + debates = {} + for row in cursor.fetchall(): + debates[row['debate_type']] = { + 'debate_type': row['debate_type'], + 'bull_arguments': row['bull_arguments'], + 'bear_arguments': row['bear_arguments'], + 'risky_arguments': row['risky_arguments'], + 'safe_arguments': row['safe_arguments'], + 'neutral_arguments': row['neutral_arguments'], + 'judge_decision': row['judge_decision'], + 'full_history': row['full_history'], + 'created_at': row['created_at'] + } + return debates + finally: + conn.close() + + +def save_pipeline_step(date: str, symbol: str, step_number: int, step_name: str, + status: str, started_at: str = None, completed_at: str = None, + duration_ms: int = None, output_summary: str = None): + """Save a pipeline step status.""" + conn = get_connection() + cursor = conn.cursor() + + try: + cursor.execute(""" + INSERT OR REPLACE INTO pipeline_steps + (date, symbol, step_number, step_name, status, + started_at, completed_at, duration_ms, output_summary) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + """, ( + date, symbol, step_number, step_name, status, + started_at, completed_at, duration_ms, output_summary + )) + conn.commit() + finally: + conn.close() + + +def save_pipeline_steps_bulk(date: str, symbol: str, steps: list): + """Save all pipeline steps at once. + + Args: + date: Date string + symbol: Stock symbol + steps: List of step dicts with step_number, step_name, status, etc. + """ + conn = get_connection() + cursor = conn.cursor() + + try: + for step in steps: + cursor.execute(""" + INSERT OR REPLACE INTO pipeline_steps + (date, symbol, step_number, step_name, status, + started_at, completed_at, duration_ms, output_summary) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + """, ( + date, symbol, + step.get('step_number'), + step.get('step_name'), + step.get('status'), + step.get('started_at'), + step.get('completed_at'), + step.get('duration_ms'), + step.get('output_summary') + )) + conn.commit() + finally: + conn.close() + + +def get_pipeline_steps(date: str, symbol: str) -> list: + """Get all pipeline steps for a stock on a date.""" + conn = get_connection() + cursor = conn.cursor() + + try: + cursor.execute(""" + SELECT * FROM pipeline_steps + WHERE date = ? AND symbol = ? + ORDER BY step_number + """, (date, symbol)) + + return [ + { + 'step_number': row['step_number'], + 'step_name': row['step_name'], + 'status': row['status'], + 'started_at': row['started_at'], + 'completed_at': row['completed_at'], + 'duration_ms': row['duration_ms'], + 'output_summary': row['output_summary'] + } + for row in cursor.fetchall() + ] + finally: + conn.close() + + +def save_data_source_log(date: str, symbol: str, source_type: str, + source_name: str, data_fetched: dict = None, + fetch_timestamp: str = None, success: bool = True, + error_message: str = None): + """Log a data source fetch.""" + conn = get_connection() + cursor = conn.cursor() + + try: + cursor.execute(""" + INSERT INTO data_source_logs + (date, symbol, source_type, source_name, data_fetched, + fetch_timestamp, success, error_message) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + """, ( + date, symbol, source_type, source_name, + json.dumps(data_fetched) if data_fetched else None, + fetch_timestamp or datetime.now().isoformat(), + 1 if success else 0, + error_message + )) + conn.commit() + finally: + conn.close() + + +def save_data_source_logs_bulk(date: str, symbol: str, logs: list): + """Save multiple data source logs at once.""" + conn = get_connection() + cursor = conn.cursor() + + try: + for log in logs: + cursor.execute(""" + INSERT INTO data_source_logs + (date, symbol, source_type, source_name, data_fetched, + fetch_timestamp, success, error_message) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + """, ( + date, symbol, + log.get('source_type'), + log.get('source_name'), + json.dumps(log.get('data_fetched')) if log.get('data_fetched') else None, + log.get('fetch_timestamp') or datetime.now().isoformat(), + 1 if log.get('success', True) else 0, + log.get('error_message') + )) + conn.commit() + finally: + conn.close() + + +def get_data_source_logs(date: str, symbol: str) -> list: + """Get all data source logs for a stock on a date.""" + conn = get_connection() + cursor = conn.cursor() + + try: + cursor.execute(""" + SELECT * FROM data_source_logs + WHERE date = ? AND symbol = ? + ORDER BY fetch_timestamp + """, (date, symbol)) + + return [ + { + 'source_type': row['source_type'], + 'source_name': row['source_name'], + 'data_fetched': json.loads(row['data_fetched']) if row['data_fetched'] else None, + 'fetch_timestamp': row['fetch_timestamp'], + 'success': bool(row['success']), + 'error_message': row['error_message'] + } + for row in cursor.fetchall() + ] + finally: + conn.close() + + +def get_full_pipeline_data(date: str, symbol: str) -> dict: + """Get complete pipeline data for a stock on a date.""" + return { + 'date': date, + 'symbol': symbol, + 'agent_reports': get_agent_reports(date, symbol), + 'debates': get_debate_history(date, symbol), + 'pipeline_steps': get_pipeline_steps(date, symbol), + 'data_sources': get_data_source_logs(date, symbol) + } + + +def save_full_pipeline_data(date: str, symbol: str, pipeline_data: dict): + """Save complete pipeline data for a stock. + + Args: + date: Date string + symbol: Stock symbol + pipeline_data: Dict containing agent_reports, debates, pipeline_steps, data_sources + """ + if 'agent_reports' in pipeline_data: + save_agent_reports_bulk(date, symbol, pipeline_data['agent_reports']) + + if 'investment_debate' in pipeline_data: + debate = pipeline_data['investment_debate'] + save_debate_history( + date, symbol, 'investment', + bull_arguments=debate.get('bull_history'), + bear_arguments=debate.get('bear_history'), + judge_decision=debate.get('judge_decision'), + full_history=debate.get('history') + ) + + if 'risk_debate' in pipeline_data: + debate = pipeline_data['risk_debate'] + save_debate_history( + date, symbol, 'risk', + risky_arguments=debate.get('risky_history'), + safe_arguments=debate.get('safe_history'), + neutral_arguments=debate.get('neutral_history'), + judge_decision=debate.get('judge_decision'), + full_history=debate.get('history') + ) + + if 'pipeline_steps' in pipeline_data: + save_pipeline_steps_bulk(date, symbol, pipeline_data['pipeline_steps']) + + if 'data_sources' in pipeline_data: + save_data_source_logs_bulk(date, symbol, pipeline_data['data_sources']) + + +def get_pipeline_summary_for_date(date: str) -> list: + """Get pipeline summary for all stocks on a date.""" + conn = get_connection() + cursor = conn.cursor() + + try: + # Get all symbols for this date + cursor.execute(""" + SELECT DISTINCT symbol FROM stock_analysis WHERE date = ? + """, (date,)) + symbols = [row['symbol'] for row in cursor.fetchall()] + + summaries = [] + for symbol in symbols: + # Get pipeline status + cursor.execute(""" + SELECT step_name, status FROM pipeline_steps + WHERE date = ? AND symbol = ? + ORDER BY step_number + """, (date, symbol)) + steps = cursor.fetchall() + + # Get agent report count + cursor.execute(""" + SELECT COUNT(*) as count FROM agent_reports + WHERE date = ? AND symbol = ? + """, (date, symbol)) + agent_count = cursor.fetchone()['count'] + + summaries.append({ + 'symbol': symbol, + 'pipeline_steps': [{'step_name': s['step_name'], 'status': s['status']} for s in steps], + 'agent_reports_count': agent_count, + 'has_debates': bool(get_debate_history(date, symbol)) + }) + + return summaries + finally: + conn.close() + + # Initialize database on module import init_db() diff --git a/frontend/backend/recommendations.db b/frontend/backend/recommendations.db index dd4d58bb170a97cc5e31f3d51be58c5960268efc..31fc1aad2adffac5026e743f43e3d53badff193b 100644 GIT binary patch literal 323584 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%0A$^q=*3?g=& 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 delta 86 zcmZp8Alz_(X@ayMD+2=q7ZAe$%S0VxX;uckszP4=9}MhVM;UlO@UP{a$Gw#|k=udm j=w?9yZ?4Tpx$IRrnD{R-@IT|fv{_K$82{!=_Ie2bDpD37 diff --git a/frontend/backend/server.py b/frontend/backend/server.py index 4a7870ee..14a8fcd6 100644 --- a/frontend/backend/server.py +++ b/frontend/backend/server.py @@ -1,9 +1,21 @@ """FastAPI server for Nifty50 AI recommendations.""" -from fastapi import FastAPI, HTTPException +from fastapi import FastAPI, HTTPException, BackgroundTasks from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel from typing import Optional import database as db +import sys +import os +from pathlib import Path +from datetime import datetime +import threading + +# Add parent directories to path for importing trading agents +PROJECT_ROOT = Path(__file__).parent.parent.parent +sys.path.insert(0, str(PROJECT_ROOT)) + +# Track running analyses +running_analyses = {} # {symbol: {"status": "running", "started_at": datetime, "progress": str}} app = FastAPI( title="Nifty50 AI API", @@ -68,19 +80,131 @@ class SaveRecommendationRequest(BaseModel): stocks_to_avoid: list +# ============== Pipeline Data Models ============== + +class AgentReport(BaseModel): + agent_type: str + report_content: str + data_sources_used: Optional[list] = [] + created_at: Optional[str] = None + + +class DebateHistory(BaseModel): + debate_type: str + bull_arguments: Optional[str] = None + bear_arguments: Optional[str] = None + risky_arguments: Optional[str] = None + safe_arguments: Optional[str] = None + neutral_arguments: Optional[str] = None + judge_decision: Optional[str] = None + full_history: Optional[str] = None + + +class PipelineStep(BaseModel): + step_number: int + step_name: str + status: str + started_at: Optional[str] = None + completed_at: Optional[str] = None + duration_ms: Optional[int] = None + output_summary: Optional[str] = None + + +class DataSourceLog(BaseModel): + source_type: str + source_name: str + data_fetched: Optional[dict] = None + fetch_timestamp: Optional[str] = None + success: bool = True + error_message: Optional[str] = None + + +class SavePipelineDataRequest(BaseModel): + date: str + symbol: str + agent_reports: Optional[dict] = None + investment_debate: Optional[dict] = None + risk_debate: Optional[dict] = None + pipeline_steps: Optional[list] = None + data_sources: Optional[list] = None + + +class RunAnalysisRequest(BaseModel): + symbol: str + date: Optional[str] = None # Defaults to today if not provided + + +def run_analysis_task(symbol: str, date: str): + """Background task to run trading analysis for a stock.""" + global running_analyses + + try: + running_analyses[symbol] = { + "status": "initializing", + "started_at": datetime.now().isoformat(), + "progress": "Loading trading agents..." + } + + # Import trading agents + from tradingagents.graph.trading_graph import TradingAgentsGraph + from tradingagents.default_config import DEFAULT_CONFIG + + running_analyses[symbol]["progress"] = "Initializing analysis pipeline..." + + # Create config + config = DEFAULT_CONFIG.copy() + config["llm_provider"] = "anthropic" # Use Claude for all LLM + config["deep_think_llm"] = "opus" # Claude Opus (Claude Max CLI alias) + config["quick_think_llm"] = "sonnet" # Claude Sonnet (Claude Max CLI alias) + config["max_debate_rounds"] = 1 + + running_analyses[symbol]["status"] = "running" + running_analyses[symbol]["progress"] = "Running market analysis..." + + # Initialize and run + ta = TradingAgentsGraph(debug=False, config=config) + + running_analyses[symbol]["progress"] = f"Analyzing {symbol}..." + final_state, decision = ta.propagate(symbol, date) + + running_analyses[symbol] = { + "status": "completed", + "completed_at": datetime.now().isoformat(), + "progress": f"Analysis complete: {decision}", + "decision": decision + } + + except Exception as e: + error_msg = str(e) if str(e) else f"{type(e).__name__}: No details provided" + running_analyses[symbol] = { + "status": "error", + "error": error_msg, + "progress": f"Error: {error_msg[:100]}" + } + import traceback + print(f"Analysis error for {symbol}: {type(e).__name__}: {error_msg}") + traceback.print_exc() + + @app.get("/") async def root(): """API root endpoint.""" return { "name": "Nifty50 AI API", - "version": "1.0.0", + "version": "2.0.0", "endpoints": { "GET /recommendations": "Get all recommendations", "GET /recommendations/latest": "Get latest recommendation", "GET /recommendations/{date}": "Get recommendation by date", + "GET /recommendations/{date}/{symbol}/pipeline": "Get full pipeline data for a stock", + "GET /recommendations/{date}/{symbol}/agents": "Get agent reports for a stock", + "GET /recommendations/{date}/{symbol}/debates": "Get debate history for a stock", + "GET /recommendations/{date}/{symbol}/data-sources": "Get data source logs for a stock", + "GET /recommendations/{date}/pipeline-summary": "Get pipeline summary for all stocks on a date", "GET /stocks/{symbol}/history": "Get stock history", "GET /dates": "Get all available dates", - "POST /recommendations": "Save a new recommendation" + "POST /recommendations": "Save a new recommendation", + "POST /pipeline": "Save pipeline data for a stock" } } @@ -146,6 +270,160 @@ async def health_check(): return {"status": "healthy", "database": "connected"} +# ============== Pipeline Data Endpoints ============== + +@app.get("/recommendations/{date}/{symbol}/pipeline") +async def get_pipeline_data(date: str, symbol: str): + """Get full pipeline data for a stock on a specific date.""" + pipeline_data = db.get_full_pipeline_data(date, symbol.upper()) + + # Check if we have any data + has_data = ( + pipeline_data.get('agent_reports') or + pipeline_data.get('debates') or + pipeline_data.get('pipeline_steps') or + pipeline_data.get('data_sources') + ) + + if not has_data: + # Return empty structure with mock pipeline steps if no data + return { + "date": date, + "symbol": symbol.upper(), + "agent_reports": {}, + "debates": {}, + "pipeline_steps": [], + "data_sources": [], + "status": "no_data" + } + + return {**pipeline_data, "status": "complete"} + + +@app.get("/recommendations/{date}/{symbol}/agents") +async def get_agent_reports(date: str, symbol: str): + """Get agent reports for a stock on a specific date.""" + reports = db.get_agent_reports(date, symbol.upper()) + return { + "date": date, + "symbol": symbol.upper(), + "reports": reports, + "count": len(reports) + } + + +@app.get("/recommendations/{date}/{symbol}/debates") +async def get_debate_history(date: str, symbol: str): + """Get debate history for a stock on a specific date.""" + debates = db.get_debate_history(date, symbol.upper()) + return { + "date": date, + "symbol": symbol.upper(), + "debates": debates + } + + +@app.get("/recommendations/{date}/{symbol}/data-sources") +async def get_data_sources(date: str, symbol: str): + """Get data source logs for a stock on a specific date.""" + logs = db.get_data_source_logs(date, symbol.upper()) + return { + "date": date, + "symbol": symbol.upper(), + "data_sources": logs, + "count": len(logs) + } + + +@app.get("/recommendations/{date}/pipeline-summary") +async def get_pipeline_summary(date: str): + """Get pipeline summary for all stocks on a specific date.""" + summary = db.get_pipeline_summary_for_date(date) + return { + "date": date, + "stocks": summary, + "count": len(summary) + } + + +@app.post("/pipeline") +async def save_pipeline_data(request: SavePipelineDataRequest): + """Save pipeline data for a stock.""" + try: + db.save_full_pipeline_data( + date=request.date, + symbol=request.symbol.upper(), + pipeline_data={ + 'agent_reports': request.agent_reports, + 'investment_debate': request.investment_debate, + 'risk_debate': request.risk_debate, + 'pipeline_steps': request.pipeline_steps, + 'data_sources': request.data_sources + } + ) + return {"message": f"Pipeline data for {request.symbol} on {request.date} saved successfully"} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +# ============== Analysis Endpoints ============== + +@app.post("/analyze/{symbol}") +async def run_analysis(symbol: str, background_tasks: BackgroundTasks, date: Optional[str] = None): + """Trigger analysis for a stock. Runs in background.""" + symbol = symbol.upper() + + # Check if analysis is already running + if symbol in running_analyses and running_analyses[symbol].get("status") == "running": + return { + "message": f"Analysis already running for {symbol}", + "status": running_analyses[symbol] + } + + # Use today's date if not provided + if not date: + date = datetime.now().strftime("%Y-%m-%d") + + # Start analysis in background thread + thread = threading.Thread(target=run_analysis_task, args=(symbol, date)) + thread.start() + + return { + "message": f"Analysis started for {symbol}", + "symbol": symbol, + "date": date, + "status": "started" + } + + +@app.get("/analyze/{symbol}/status") +async def get_analysis_status(symbol: str): + """Get the status of a running or completed analysis.""" + symbol = symbol.upper() + + if symbol not in running_analyses: + return { + "symbol": symbol, + "status": "not_started", + "message": "No analysis has been run for this stock" + } + + return { + "symbol": symbol, + **running_analyses[symbol] + } + + +@app.get("/analyze/running") +async def get_running_analyses(): + """Get all currently running analyses.""" + running = {k: v for k, v in running_analyses.items() if v.get("status") == "running"} + return { + "running": running, + "count": len(running) + } + + if __name__ == "__main__": import uvicorn - uvicorn.run(app, host="0.0.0.0", port=8000) + uvicorn.run(app, host="0.0.0.0", port=8001) diff --git a/frontend/package.json b/frontend/package.json index 498c5815..360f155c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -30,6 +30,7 @@ "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", "globals": "^16.5.0", + "playwright": "^1.58.1", "postcss": "^8.5.6", "puppeteer": "^24.36.1", "tailwindcss": "^4.1.18", diff --git a/frontend/src/components/pipeline/AgentReportCard.tsx b/frontend/src/components/pipeline/AgentReportCard.tsx new file mode 100644 index 00000000..4b5610b0 --- /dev/null +++ b/frontend/src/components/pipeline/AgentReportCard.tsx @@ -0,0 +1,194 @@ +import { useState } from 'react'; +import { + TrendingUp, Newspaper, Users, FileText, + ChevronDown, ChevronUp, Database, Clock, CheckCircle +} from 'lucide-react'; +import type { AgentReport, AgentType } from '../../types/pipeline'; +import { AGENT_METADATA } from '../../types/pipeline'; + +interface AgentReportCardProps { + agentType: AgentType; + report?: AgentReport; + isLoading?: boolean; +} + +const AGENT_ICONS: Record = { + market: TrendingUp, + news: Newspaper, + social_media: Users, + fundamentals: FileText, +}; + +const AGENT_COLORS: Record = { + market: { + bg: 'bg-blue-50 dark:bg-blue-900/20', + border: 'border-blue-200 dark:border-blue-800', + text: 'text-blue-700 dark:text-blue-300', + accent: 'bg-blue-500' + }, + news: { + bg: 'bg-purple-50 dark:bg-purple-900/20', + border: 'border-purple-200 dark:border-purple-800', + text: 'text-purple-700 dark:text-purple-300', + accent: 'bg-purple-500' + }, + social_media: { + bg: 'bg-pink-50 dark:bg-pink-900/20', + border: 'border-pink-200 dark:border-pink-800', + text: 'text-pink-700 dark:text-pink-300', + accent: 'bg-pink-500' + }, + fundamentals: { + bg: 'bg-green-50 dark:bg-green-900/20', + border: 'border-green-200 dark:border-green-800', + text: 'text-green-700 dark:text-green-300', + accent: 'bg-green-500' + }, +}; + +export function AgentReportCard({ agentType, report, isLoading }: AgentReportCardProps) { + const [isExpanded, setIsExpanded] = useState(false); + + const Icon = AGENT_ICONS[agentType]; + const colors = AGENT_COLORS[agentType]; + const metadata = AGENT_METADATA[agentType]; + const hasReport = report && report.report_content; + + // Parse markdown-like content into sections + const parseContent = (content: string) => { + const lines = content.split('\n'); + const sections: { title: string; content: string[] }[] = []; + let currentSection: { title: string; content: string[] } | null = null; + + lines.forEach(line => { + if (line.startsWith('##') || line.startsWith('**')) { + if (currentSection) { + sections.push(currentSection); + } + currentSection = { + title: line.replace(/^#+\s*/, '').replace(/\*\*/g, ''), + content: [] + }; + } else if (currentSection && line.trim()) { + currentSection.content.push(line); + } + }); + + if (currentSection) { + sections.push(currentSection); + } + + return sections; + }; + + const sections = hasReport ? parseContent(report.report_content) : []; + const previewText = hasReport + ? report.report_content.slice(0, 200).replace(/[#*]/g, '') + '...' + : 'No analysis available'; + + return ( +

+ {/* Header */} +
hasReport && setIsExpanded(!isExpanded)} + > +
+
+ +
+
+

{metadata.label}

+

+ {metadata.description} +

+
+
+ +
+ {hasReport ? ( + + ) : isLoading ? ( +
+ ) : ( + + )} + + {hasReport && ( + isExpanded ? ( + + ) : ( + + ) + )} +
+
+ + {/* Preview (collapsed) */} + {!isExpanded && hasReport && ( +
+

+ {previewText} +

+
+ )} + + {/* Expanded content */} + {isExpanded && hasReport && ( +
+ {/* Data sources */} + {report.data_sources_used && report.data_sources_used.length > 0 && ( +
+ + Sources: + {report.data_sources_used.map((source, idx) => ( + + {source} + + ))} +
+ )} + + {/* Report content */} +
+ {sections.length > 0 ? ( + sections.map((section, idx) => ( +
+

+ {section.title} +

+
+ {section.content.map((line, lineIdx) => ( +

{line}

+ ))} +
+
+ )) + ) : ( +
+
+                  {report.report_content}
+                
+
+ )} +
+ + {/* Timestamp */} + {report.created_at && ( +
+ + + Generated: {new Date(report.created_at).toLocaleString()} + +
+ )} +
+ )} +
+ ); +} + +export default AgentReportCard; diff --git a/frontend/src/components/pipeline/DataSourcesPanel.tsx b/frontend/src/components/pipeline/DataSourcesPanel.tsx new file mode 100644 index 00000000..1c4f93f1 --- /dev/null +++ b/frontend/src/components/pipeline/DataSourcesPanel.tsx @@ -0,0 +1,185 @@ +import { useState } from 'react'; +import { + Database, ChevronDown, ChevronUp, CheckCircle, + XCircle, Clock, ExternalLink, Server +} from 'lucide-react'; +import type { DataSourceLog } from '../../types/pipeline'; + +interface DataSourcesPanelProps { + dataSources: DataSourceLog[]; + isLoading?: boolean; +} + +const SOURCE_TYPE_COLORS: Record = { + market_data: { bg: 'bg-blue-100 dark:bg-blue-900/30', text: 'text-blue-700 dark:text-blue-300' }, + news: { bg: 'bg-purple-100 dark:bg-purple-900/30', text: 'text-purple-700 dark:text-purple-300' }, + fundamentals: { bg: 'bg-green-100 dark:bg-green-900/30', text: 'text-green-700 dark:text-green-300' }, + social_media: { bg: 'bg-pink-100 dark:bg-pink-900/30', text: 'text-pink-700 dark:text-pink-300' }, + indicators: { bg: 'bg-amber-100 dark:bg-amber-900/30', text: 'text-amber-700 dark:text-amber-300' }, + default: { bg: 'bg-slate-100 dark:bg-slate-800', text: 'text-slate-700 dark:text-slate-300' } +}; + +export function DataSourcesPanel({ dataSources, isLoading }: DataSourcesPanelProps) { + const [isExpanded, setIsExpanded] = useState(false); + const [expandedSources, setExpandedSources] = useState>(new Set()); + + const hasData = dataSources.length > 0; + const successCount = dataSources.filter(s => s.success).length; + const errorCount = dataSources.filter(s => !s.success).length; + + const toggleSourceExpanded = (index: number) => { + const newSet = new Set(expandedSources); + if (newSet.has(index)) { + newSet.delete(index); + } else { + newSet.add(index); + } + setExpandedSources(newSet); + }; + + const getSourceColors = (sourceType: string) => { + return SOURCE_TYPE_COLORS[sourceType] || SOURCE_TYPE_COLORS.default; + }; + + const formatTimestamp = (timestamp?: string) => { + if (!timestamp) return 'Unknown'; + try { + return new Date(timestamp).toLocaleString(); + } catch { + return timestamp; + } + }; + + return ( +
+ {/* Header */} +
hasData && setIsExpanded(!isExpanded)} + > +
+
+ +
+
+

+ Data Sources +

+

+ Raw data fetched for analysis +

+
+
+ +
+ {hasData ? ( +
+ + + {successCount} + + {errorCount > 0 && ( + + + {errorCount} + + )} +
+ ) : isLoading ? ( +
+ ) : ( + + No Data + + )} + {hasData && ( + isExpanded ? : + )} +
+
+ + {/* Expanded content */} + {isExpanded && hasData && ( +
+
+ {dataSources.map((source, index) => { + const colors = getSourceColors(source.source_type); + const isSourceExpanded = expandedSources.has(index); + + return ( +
+ {/* Source header */} +
toggleSourceExpanded(index)} + > +
+ +
+
+ + {source.source_type} + + + {source.source_name} + +
+
+ + {formatTimestamp(source.fetch_timestamp)} +
+
+
+ +
+ {source.success ? ( + + ) : ( + + )} + {isSourceExpanded ? ( + + ) : ( + + )} +
+
+ + {/* Source details (expanded) */} + {isSourceExpanded && ( +
+ {source.error_message ? ( +
+

+ Error: {source.error_message} +

+
+ ) : source.data_fetched ? ( +
+

+ Data Summary: +

+
+                            {typeof source.data_fetched === 'string'
+                              ? source.data_fetched.slice(0, 500) + (source.data_fetched.length > 500 ? '...' : '')
+                              : JSON.stringify(source.data_fetched, null, 2).slice(0, 500)}
+                          
+
+ ) : ( +

+ No data details available +

+ )} +
+ )} +
+ ); + })} +
+
+ )} +
+ ); +} + +export default DataSourcesPanel; diff --git a/frontend/src/components/pipeline/DebateViewer.tsx b/frontend/src/components/pipeline/DebateViewer.tsx new file mode 100644 index 00000000..b9f9f385 --- /dev/null +++ b/frontend/src/components/pipeline/DebateViewer.tsx @@ -0,0 +1,254 @@ +import { useState } from 'react'; +import { + TrendingUp, TrendingDown, Scale, ChevronDown, ChevronUp, + MessageSquare, Award, Clock +} from 'lucide-react'; +import type { DebateHistory } from '../../types/pipeline'; + +interface DebateViewerProps { + debate?: DebateHistory; + isLoading?: boolean; +} + +export function DebateViewer({ debate, isLoading }: DebateViewerProps) { + const [isExpanded, setIsExpanded] = useState(false); + const [activeTab, setActiveTab] = useState<'bull' | 'bear' | 'history'>('history'); + + const hasDebate = debate && (debate.bull_arguments || debate.bear_arguments || debate.full_history); + + // Parse debate rounds from full history + const parseDebateRounds = (history: string) => { + const rounds: { speaker: string; content: string }[] = []; + const lines = history.split('\n'); + + let currentSpeaker = ''; + let currentContent: string[] = []; + + lines.forEach(line => { + if (line.startsWith('Bull') || line.startsWith('Bear') || line.startsWith('Judge')) { + if (currentSpeaker && currentContent.length > 0) { + rounds.push({ + speaker: currentSpeaker, + content: currentContent.join('\n') + }); + } + currentSpeaker = line.split(':')[0] || line.split(' ')[0]; + currentContent = [line.substring(line.indexOf(':') + 1).trim()]; + } else if (line.trim()) { + currentContent.push(line); + } + }); + + if (currentSpeaker && currentContent.length > 0) { + rounds.push({ + speaker: currentSpeaker, + content: currentContent.join('\n') + }); + } + + return rounds; + }; + + const debateRounds = hasDebate && debate.full_history + ? parseDebateRounds(debate.full_history) + : []; + + const getSpeakerStyle = (speaker: string) => { + if (speaker.toLowerCase().includes('bull')) { + return { + bg: 'bg-green-50 dark:bg-green-900/20', + border: 'border-l-green-500', + icon: TrendingUp, + color: 'text-green-600 dark:text-green-400' + }; + } else if (speaker.toLowerCase().includes('bear')) { + return { + bg: 'bg-red-50 dark:bg-red-900/20', + border: 'border-l-red-500', + icon: TrendingDown, + color: 'text-red-600 dark:text-red-400' + }; + } else { + return { + bg: 'bg-blue-50 dark:bg-blue-900/20', + border: 'border-l-blue-500', + icon: Scale, + color: 'text-blue-600 dark:text-blue-400' + }; + } + }; + + return ( +
+ {/* Header */} +
hasDebate && setIsExpanded(!isExpanded)} + > +
+
+
+ +
+
+ +
+
+ +
+
+
+

+ Investment Debate +

+

+ Bull vs Bear Analysis with Research Manager Decision +

+
+
+ +
+ {hasDebate ? ( + + Complete + + ) : isLoading ? ( +
+ ) : ( + + No Data + + )} + {hasDebate && ( + isExpanded ? : + )} +
+
+ + {/* Expanded content */} + {isExpanded && hasDebate && ( +
+ {/* Tabs */} +
+ + + +
+ + {/* Content */} +
+ {activeTab === 'history' && ( +
+ {debateRounds.length > 0 ? ( + debateRounds.map((round, idx) => { + const style = getSpeakerStyle(round.speaker); + const Icon = style.icon; + return ( +
+
+ + + {round.speaker} + +
+

+ {round.content} +

+
+ ); + }) + ) : debate.full_history ? ( +
+                    {debate.full_history}
+                  
+ ) : ( +

No debate history available

+ )} +
+ )} + + {activeTab === 'bull' && ( +
+
+ + + Bull Analyst Arguments + +
+

+ {debate.bull_arguments || 'No bull arguments recorded'} +

+
+ )} + + {activeTab === 'bear' && ( +
+
+ + + Bear Analyst Arguments + +
+

+ {debate.bear_arguments || 'No bear arguments recorded'} +

+
+ )} +
+ + {/* Judge Decision */} + {debate.judge_decision && ( +
+
+ +
+

+ Research Manager Decision +

+

+ {debate.judge_decision} +

+
+
+
+ )} +
+ )} +
+ ); +} + +export default DebateViewer; diff --git a/frontend/src/components/pipeline/PipelineOverview.tsx b/frontend/src/components/pipeline/PipelineOverview.tsx new file mode 100644 index 00000000..78e6157b --- /dev/null +++ b/frontend/src/components/pipeline/PipelineOverview.tsx @@ -0,0 +1,157 @@ +import { + Database, TrendingUp, Newspaper, Users, FileText, + MessageSquare, Target, Shield, CheckCircle, Loader2, + AlertCircle, Clock +} from 'lucide-react'; +import type { PipelineStep, PipelineStepStatus } from '../../types/pipeline'; + +interface PipelineOverviewProps { + steps: PipelineStep[]; + onStepClick?: (step: PipelineStep) => void; + compact?: boolean; +} + +const STEP_ICONS: Record = { + data_collection: Database, + market_analysis: TrendingUp, + news_analysis: Newspaper, + social_analysis: Users, + fundamentals_analysis: FileText, + investment_debate: MessageSquare, + trader_decision: Target, + risk_debate: Shield, + final_decision: CheckCircle, +}; + +const STEP_LABELS: Record = { + data_collection: 'Data Collection', + market_analysis: 'Market Analysis', + news_analysis: 'News Analysis', + social_analysis: 'Social Analysis', + fundamentals_analysis: 'Fundamentals', + investment_debate: 'Investment Debate', + trader_decision: 'Trader Decision', + risk_debate: 'Risk Assessment', + final_decision: 'Final Decision', +}; + +const STATUS_STYLES: Record = { + pending: { + bg: 'bg-slate-100 dark:bg-slate-800', + border: 'border-slate-300 dark:border-slate-600', + text: 'text-slate-400 dark:text-slate-500', + icon: Clock + }, + running: { + bg: 'bg-blue-50 dark:bg-blue-900/30', + border: 'border-blue-400 dark:border-blue-500', + text: 'text-blue-600 dark:text-blue-400', + icon: Loader2 + }, + completed: { + bg: 'bg-green-50 dark:bg-green-900/30', + border: 'border-green-400 dark:border-green-500', + text: 'text-green-600 dark:text-green-400', + icon: CheckCircle + }, + error: { + bg: 'bg-red-50 dark:bg-red-900/30', + border: 'border-red-400 dark:border-red-500', + text: 'text-red-600 dark:text-red-400', + icon: AlertCircle + }, +}; + +// Default pipeline steps when no data is available +const DEFAULT_STEPS: PipelineStep[] = [ + { step_number: 1, step_name: 'data_collection', status: 'pending' }, + { step_number: 2, step_name: 'market_analysis', status: 'pending' }, + { step_number: 3, step_name: 'news_analysis', status: 'pending' }, + { step_number: 4, step_name: 'social_analysis', status: 'pending' }, + { step_number: 5, step_name: 'fundamentals_analysis', status: 'pending' }, + { step_number: 6, step_name: 'investment_debate', status: 'pending' }, + { step_number: 7, step_name: 'trader_decision', status: 'pending' }, + { step_number: 8, step_name: 'risk_debate', status: 'pending' }, + { step_number: 9, step_name: 'final_decision', status: 'pending' }, +]; + +export function PipelineOverview({ steps, onStepClick, compact = false }: PipelineOverviewProps) { + const displaySteps = steps.length > 0 ? steps : DEFAULT_STEPS; + + const completedCount = displaySteps.filter(s => s.status === 'completed').length; + const totalSteps = displaySteps.length; + const progress = Math.round((completedCount / totalSteps) * 100); + + if (compact) { + return ( +
+ {displaySteps.map((step, index) => { + const styles = STATUS_STYLES[step.status]; + return ( +
+ ); + })} + {progress}% +
+ ); + } + + return ( +
+ {/* Progress bar */} +
+
+
+
+ + {completedCount}/{totalSteps} + +
+ + {/* Pipeline steps */} +
+ {displaySteps.map((step, index) => { + const StepIcon = STEP_ICONS[step.step_name] || Database; + const styles = STATUS_STYLES[step.status]; + const StatusIcon = styles.icon; + const label = STEP_LABELS[step.step_name] || step.step_name; + + return ( + + ); + })} +
+
+ ); +} + +export default PipelineOverview; diff --git a/frontend/src/components/pipeline/RiskDebateViewer.tsx b/frontend/src/components/pipeline/RiskDebateViewer.tsx new file mode 100644 index 00000000..98d32b3b --- /dev/null +++ b/frontend/src/components/pipeline/RiskDebateViewer.tsx @@ -0,0 +1,256 @@ +import { useState } from 'react'; +import { + Zap, Shield, Scale, ChevronDown, ChevronUp, + ShieldCheck, AlertTriangle +} from 'lucide-react'; +import type { DebateHistory } from '../../types/pipeline'; + +interface RiskDebateViewerProps { + debate?: DebateHistory; + isLoading?: boolean; +} + +export function RiskDebateViewer({ debate, isLoading }: RiskDebateViewerProps) { + const [isExpanded, setIsExpanded] = useState(false); + const [activeTab, setActiveTab] = useState<'all' | 'risky' | 'safe' | 'neutral'>('all'); + + const hasDebate = debate && ( + debate.risky_arguments || + debate.safe_arguments || + debate.neutral_arguments || + debate.full_history + ); + + const ROLE_STYLES = { + risky: { + bg: 'bg-red-50 dark:bg-red-900/20', + border: 'border-l-red-500', + icon: Zap, + color: 'text-red-600 dark:text-red-400', + label: 'Aggressive Analyst' + }, + safe: { + bg: 'bg-green-50 dark:bg-green-900/20', + border: 'border-l-green-500', + icon: Shield, + color: 'text-green-600 dark:text-green-400', + label: 'Conservative Analyst' + }, + neutral: { + bg: 'bg-slate-50 dark:bg-slate-800/50', + border: 'border-l-slate-500', + icon: Scale, + color: 'text-slate-600 dark:text-slate-400', + label: 'Neutral Analyst' + } + }; + + return ( +
+ {/* Header */} +
hasDebate && setIsExpanded(!isExpanded)} + > +
+
+
+ +
+
+ +
+
+ +
+
+
+

+ Risk Assessment Debate +

+

+ Aggressive vs Conservative vs Neutral with Risk Manager Decision +

+
+
+ +
+ {hasDebate ? ( + + Complete + + ) : isLoading ? ( +
+ ) : ( + + No Data + + )} + {hasDebate && ( + isExpanded ? : + )} +
+
+ + {/* Expanded content */} + {isExpanded && hasDebate && ( +
+ {/* Tabs */} +
+ + + + +
+ + {/* Content */} +
+ {activeTab === 'all' && ( +
+ {/* Aggressive */} +
+
+ + + {ROLE_STYLES.risky.label} + +
+

+ {debate.risky_arguments || 'No arguments recorded'} +

+
+ + {/* Neutral */} +
+
+ + + {ROLE_STYLES.neutral.label} + +
+

+ {debate.neutral_arguments || 'No arguments recorded'} +

+
+ + {/* Conservative */} +
+
+ + + {ROLE_STYLES.safe.label} + +
+

+ {debate.safe_arguments || 'No arguments recorded'} +

+
+
+ )} + + {activeTab === 'risky' && ( +
+
+ + + {ROLE_STYLES.risky.label} + + +
+

+ {debate.risky_arguments || 'No aggressive arguments recorded'} +

+
+ )} + + {activeTab === 'neutral' && ( +
+
+ + + {ROLE_STYLES.neutral.label} + +
+

+ {debate.neutral_arguments || 'No neutral arguments recorded'} +

+
+ )} + + {activeTab === 'safe' && ( +
+
+ + + {ROLE_STYLES.safe.label} + +
+

+ {debate.safe_arguments || 'No conservative arguments recorded'} +

+
+ )} +
+ + {/* Risk Manager Decision */} + {debate.judge_decision && ( +
+
+ +
+

+ Risk Manager Decision +

+

+ {debate.judge_decision} +

+
+
+
+ )} +
+ )} +
+ ); +} + +export default RiskDebateViewer; diff --git a/frontend/src/components/pipeline/index.ts b/frontend/src/components/pipeline/index.ts new file mode 100644 index 00000000..e33595ab --- /dev/null +++ b/frontend/src/components/pipeline/index.ts @@ -0,0 +1,5 @@ +export { PipelineOverview } from './PipelineOverview'; +export { AgentReportCard } from './AgentReportCard'; +export { DebateViewer } from './DebateViewer'; +export { RiskDebateViewer } from './RiskDebateViewer'; +export { DataSourcesPanel } from './DataSourcesPanel'; diff --git a/frontend/src/pages/StockDetail.tsx b/frontend/src/pages/StockDetail.tsx index 9385a538..2bc52e14 100644 --- a/frontend/src/pages/StockDetail.tsx +++ b/frontend/src/pages/StockDetail.tsx @@ -1,14 +1,40 @@ import { useParams, Link } from 'react-router-dom'; -import { useMemo } from 'react'; -import { ArrowLeft, Building2, TrendingUp, TrendingDown, Minus, AlertTriangle, Calendar, Activity, LineChart } from 'lucide-react'; +import { useMemo, useState, useEffect } from 'react'; +import { + ArrowLeft, Building2, TrendingUp, TrendingDown, Minus, AlertTriangle, + Calendar, Activity, LineChart, Database, MessageSquare, FileText, Layers, + RefreshCw, Play, Loader2 +} from 'lucide-react'; import { NIFTY_50_STOCKS } from '../types'; import { sampleRecommendations, getStockHistory, getExtendedPriceHistory, getPredictionPointsWithPrices, getRawAnalysis } from '../data/recommendations'; import { DecisionBadge, ConfidenceBadge, RiskBadge } from '../components/StockCard'; import AIAnalysisPanel from '../components/AIAnalysisPanel'; import StockPriceChart from '../components/StockPriceChart'; +import { + PipelineOverview, + AgentReportCard, + DebateViewer, + RiskDebateViewer, + DataSourcesPanel +} from '../components/pipeline'; +import { api } from '../services/api'; +import type { FullPipelineData, AgentType } from '../types/pipeline'; + +type TabType = 'overview' | 'pipeline' | 'debates' | 'data'; export default function StockDetail() { const { symbol } = useParams<{ symbol: string }>(); + const [activeTab, setActiveTab] = useState('overview'); + const [pipelineData, setPipelineData] = useState(null); + const [isLoadingPipeline, setIsLoadingPipeline] = useState(false); + const [isRefreshing, setIsRefreshing] = useState(false); + const [lastRefresh, setLastRefresh] = useState(null); + const [refreshMessage, setRefreshMessage] = useState(null); + + // Analysis state + const [isAnalysisRunning, setIsAnalysisRunning] = useState(false); + const [analysisStatus, setAnalysisStatus] = useState(null); + const [analysisProgress, setAnalysisProgress] = useState(null); const stock = NIFTY_50_STOCKS.find(s => s.symbol === symbol); const latestRecommendation = sampleRecommendations[0]; @@ -26,6 +52,119 @@ export default function StockDetail() { : []; }, [symbol, priceHistory]); + // Function to fetch pipeline data + const fetchPipelineData = async (forceRefresh = false) => { + if (!symbol || !latestRecommendation?.date) return; + + if (forceRefresh) { + setIsRefreshing(true); + } else { + setIsLoadingPipeline(true); + } + + try { + const data = await api.getPipelineData(latestRecommendation.date, symbol, forceRefresh); + setPipelineData(data); + if (forceRefresh) { + setLastRefresh(new Date().toLocaleTimeString()); + const hasData = data.pipeline_steps?.length > 0 || Object.keys(data.agent_reports || {}).length > 0; + setRefreshMessage(hasData ? `✓ Data refreshed for ${symbol}` : `No pipeline data found for ${symbol}`); + setTimeout(() => setRefreshMessage(null), 3000); + } + console.log('Pipeline data fetched:', data); + } catch (error) { + console.error('Failed to fetch pipeline data:', error); + if (forceRefresh) { + setRefreshMessage(`✗ Failed to refresh: ${error}`); + setTimeout(() => setRefreshMessage(null), 3000); + } + // Set empty pipeline data structure + setPipelineData({ + date: latestRecommendation.date, + symbol: symbol, + agent_reports: {}, + debates: {}, + pipeline_steps: [], + data_sources: [], + status: 'no_data' + }); + } finally { + setIsLoadingPipeline(false); + setIsRefreshing(false); + } + }; + + // Fetch pipeline data when tab changes or symbol changes + useEffect(() => { + if (activeTab === 'overview') return; // Don't fetch for overview tab + fetchPipelineData(); + }, [symbol, latestRecommendation?.date, activeTab]); + + // Refresh handler + const handleRefresh = async () => { + console.log('Refresh button clicked - fetching fresh data...'); + await fetchPipelineData(true); + console.log('Refresh complete - data updated'); + }; + + // Run Analysis handler + const handleRunAnalysis = async () => { + if (!symbol || !latestRecommendation?.date) return; + + setIsAnalysisRunning(true); + setAnalysisStatus('starting'); + setAnalysisProgress('Starting analysis...'); + + try { + // Trigger analysis + await api.runAnalysis(symbol, latestRecommendation.date); + setAnalysisStatus('running'); + + // Poll for status + const pollInterval = setInterval(async () => { + try { + const status = await api.getAnalysisStatus(symbol); + setAnalysisProgress(status.progress || 'Processing...'); + + if (status.status === 'completed') { + clearInterval(pollInterval); + setIsAnalysisRunning(false); + setAnalysisStatus('completed'); + setAnalysisProgress(`✓ Analysis complete: ${status.decision || 'Done'}`); + // Refresh data to show results + await fetchPipelineData(true); + setTimeout(() => { + setAnalysisProgress(null); + setAnalysisStatus(null); + }, 5000); + } else if (status.status === 'error') { + clearInterval(pollInterval); + setIsAnalysisRunning(false); + setAnalysisStatus('error'); + setAnalysisProgress(`✗ Error: ${status.error}`); + } + } catch (err) { + console.error('Failed to poll analysis status:', err); + } + }, 2000); // Poll every 2 seconds + + // Cleanup after 10 minutes max + setTimeout(() => clearInterval(pollInterval), 600000); + + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + console.error('Failed to start analysis:', errorMessage, error); + setIsAnalysisRunning(false); + setAnalysisStatus('error'); + // More helpful error message + if (errorMessage.includes('Failed to fetch') || errorMessage.includes('NetworkError')) { + setAnalysisProgress(`✗ Network error: Cannot connect to backend at localhost:8000. Please check if the server is running.`); + } else { + setAnalysisProgress(`✗ Failed to start analysis: ${errorMessage}`); + } + } + }; + if (!stock) { return (
@@ -56,6 +195,13 @@ export default function StockDetail() { const DecisionIcon = analysis?.decision ? decisionIcon[analysis.decision] : Activity; const bgGradient = analysis?.decision ? decisionColor[analysis.decision] : 'from-gray-500 to-gray-600'; + const TABS = [ + { id: 'overview' as const, label: 'Overview', icon: LineChart }, + { id: 'pipeline' as const, label: 'Analysis Pipeline', icon: Layers }, + { id: 'debates' as const, label: 'Debates', icon: MessageSquare }, + { id: 'data' as const, label: 'Data Sources', icon: Database }, + ]; + return (
{/* Back Button */} @@ -120,89 +266,266 @@ export default function StockDetail() { )} - {/* Price Chart with Predictions */} - {priceHistory.length > 0 && ( -
-
-
- -

Price History & AI Predictions

-
-
-
- -
-
- )} + {/* Tab Navigation */} +
+ {TABS.map(tab => { + const Icon = tab.icon; + const isActive = activeTab === tab.id; + return ( + + ); + })} - {/* AI Analysis Panel */} - {analysis && getRawAnalysis(symbol || '') && ( - - )} + {/* Action Buttons - Show on non-overview tabs */} + {activeTab !== 'overview' && ( +
+ {lastRefresh && ( + + Updated: {lastRefresh} + + )} - {/* Compact Stats Grid */} -
-
-
{history.length}
-
Analyses
-
-
-
- {history.filter((h: { decision: string }) => h.decision === 'BUY').length} -
-
Buy
-
-
-
- {history.filter((h: { decision: string }) => h.decision === 'HOLD').length} -
-
Hold
-
-
-
- {history.filter((h: { decision: string }) => h.decision === 'SELL').length} -
-
Sell
-
-
+ {/* Run Analysis Button */} + - {/* Analysis History */} -
-
-

Recommendation History

-
- - {history.length > 0 ? ( -
- {history.map((entry, idx) => ( -
-
- {new Date(entry.date).toLocaleDateString('en-IN', { - weekday: 'short', - month: 'short', - day: 'numeric', - })} -
- -
- ))} -
- ) : ( -
- -

No history yet

+ {/* Refresh Button */} +
)} -
+
- {/* Top Pick / Avoid Status - Compact */} + {/* Analysis Progress Banner */} + {analysisProgress && ( +
+ {isAnalysisRunning && } + {analysisProgress} +
+ )} + + {/* Refresh Notification */} + {refreshMessage && !analysisProgress && ( +
+ {refreshMessage} +
+ )} + + {/* Tab Content */} + {activeTab === 'overview' && ( + <> + {/* Price Chart with Predictions */} + {priceHistory.length > 0 && ( +
+
+
+ +

Price History & AI Predictions

+
+
+
+ +
+
+ )} + + {/* AI Analysis Panel */} + {analysis && getRawAnalysis(symbol || '') && ( + + )} + + {/* Compact Stats Grid */} +
+
+
{history.length}
+
Analyses
+
+
+
+ {history.filter((h: { decision: string }) => h.decision === 'BUY').length} +
+
Buy
+
+
+
+ {history.filter((h: { decision: string }) => h.decision === 'HOLD').length} +
+
Hold
+
+
+
+ {history.filter((h: { decision: string }) => h.decision === 'SELL').length} +
+
Sell
+
+
+ + {/* Analysis History */} +
+
+

Recommendation History

+
+ + {history.length > 0 ? ( +
+ {history.map((entry, idx) => ( +
+
+ {new Date(entry.date).toLocaleDateString('en-IN', { + weekday: 'short', + month: 'short', + day: 'numeric', + })} +
+ +
+ ))} +
+ ) : ( +
+ +

No history yet

+
+ )} +
+ + )} + + {activeTab === 'pipeline' && ( +
+ {/* Pipeline Overview */} +
+
+ +

Analysis Pipeline

+
+ console.log('Step clicked:', step)} + /> +
+ + {/* Agent Reports Grid */} +
+
+ +

Agent Reports

+
+
+ {(['market', 'news', 'social_media', 'fundamentals'] as AgentType[]).map(agentType => ( + + ))} +
+
+
+ )} + + {activeTab === 'debates' && ( +
+ {/* Investment Debate */} + + + {/* Risk Debate */} + +
+ )} + + {activeTab === 'data' && ( +
+ + + {/* No data message */} + {!isLoadingPipeline && (!pipelineData?.data_sources || pipelineData.data_sources.length === 0) && ( +
+ +

+ No Data Source Logs Available +

+

+ Data source logs will appear here when the analysis pipeline runs. + This includes information about market data, news, and fundamental data fetched. +

+
+ )} +
+ )} + + {/* Top Pick / Avoid Status - Compact (visible on all tabs) */} {latestRecommendation && ( <> {latestRecommendation.top_picks.some(p => p.symbol === symbol) && ( diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index 1bb884cc..b9201fc2 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -1,8 +1,28 @@ /** * API service for fetching stock recommendations from the backend. + * Updated with cache-busting for refresh functionality. */ -const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000'; +import type { + FullPipelineData, + AgentReportsMap, + DebatesMap, + DataSourceLog, + PipelineSummary +} from '../types/pipeline'; + +// Use same hostname as the page, just different port for API +const getApiBaseUrl = () => { + // If env variable is set, use it + if (import.meta.env.VITE_API_URL) { + return import.meta.env.VITE_API_URL; + } + // Otherwise use the same host as the current page with port 8001 + const hostname = typeof window !== 'undefined' ? window.location.hostname : 'localhost'; + return `http://${hostname}:8001`; +}; + +const API_BASE_URL = getApiBaseUrl(); export interface StockAnalysis { symbol: string; @@ -57,14 +77,26 @@ class ApiService { this.baseUrl = API_BASE_URL; } - private async fetch(endpoint: string, options?: RequestInit): Promise { - const url = `${this.baseUrl}${endpoint}`; + private async fetch(endpoint: string, options?: RequestInit & { noCache?: boolean }): Promise { + let url = `${this.baseUrl}${endpoint}`; + + // Add cache-busting query param if noCache is true + const noCache = options?.noCache; + if (noCache) { + const separator = url.includes('?') ? '&' : '?'; + url = `${url}${separator}_t=${Date.now()}`; + } + + // Remove noCache from options before passing to fetch + const { noCache: _, ...fetchOptions } = options || {}; + const response = await fetch(url, { - ...options, + ...fetchOptions, headers: { 'Content-Type': 'application/json', - ...options?.headers, + ...fetchOptions?.headers, }, + cache: noCache ? 'no-store' : undefined, }); if (!response.ok) { @@ -131,6 +163,127 @@ class ApiService { body: JSON.stringify(recommendation), }); } + + // ============== Pipeline Data Methods ============== + + /** + * Get full pipeline data for a stock on a specific date + */ + async getPipelineData(date: string, symbol: string, refresh = false): Promise { + return this.fetch(`/recommendations/${date}/${symbol}/pipeline`, { noCache: refresh }); + } + + /** + * Get agent reports for a stock on a specific date + */ + async getAgentReports(date: string, symbol: string): Promise<{ + date: string; + symbol: string; + reports: AgentReportsMap; + count: number; + }> { + return this.fetch(`/recommendations/${date}/${symbol}/agents`); + } + + /** + * Get debate history for a stock on a specific date + */ + async getDebateHistory(date: string, symbol: string): Promise<{ + date: string; + symbol: string; + debates: DebatesMap; + }> { + return this.fetch(`/recommendations/${date}/${symbol}/debates`); + } + + /** + * Get data source logs for a stock on a specific date + */ + async getDataSources(date: string, symbol: string): Promise<{ + date: string; + symbol: string; + data_sources: DataSourceLog[]; + count: number; + }> { + return this.fetch(`/recommendations/${date}/${symbol}/data-sources`); + } + + /** + * Get pipeline summary for all stocks on a specific date + */ + async getPipelineSummary(date: string): Promise<{ + date: string; + stocks: PipelineSummary[]; + count: number; + }> { + return this.fetch(`/recommendations/${date}/pipeline-summary`); + } + + /** + * Save pipeline data for a stock (used by the analyzer) + */ + async savePipelineData(data: { + date: string; + symbol: string; + agent_reports?: Record; + investment_debate?: Record; + risk_debate?: Record; + pipeline_steps?: unknown[]; + data_sources?: unknown[]; + }): Promise<{ message: string }> { + return this.fetch('/pipeline', { + method: 'POST', + body: JSON.stringify(data), + }); + } + + // ============== Analysis Trigger Methods ============== + + /** + * Start analysis for a stock + */ + async runAnalysis(symbol: string, date?: string): Promise<{ + message: string; + symbol: string; + date: string; + status: string; + }> { + const url = date ? `/analyze/${symbol}?date=${date}` : `/analyze/${symbol}`; + return this.fetch(url, { + method: 'POST', + body: JSON.stringify({}), + noCache: true, + headers: { + 'Cache-Control': 'no-cache, no-store, must-revalidate', + 'Pragma': 'no-cache' + } + }); + } + + /** + * Get analysis status for a stock + */ + async getAnalysisStatus(symbol: string): Promise<{ + symbol: string; + status: string; + progress?: string; + error?: string; + decision?: string; + started_at?: string; + completed_at?: string; + }> { + return this.fetch(`/analyze/${symbol}/status`, { noCache: true }); + } + + /** + * Get all running analyses + */ + async getRunningAnalyses(): Promise<{ + running: Record; + count: number; + }> { + return this.fetch('/analyze/running', { noCache: true }); + } } export const api = new ApiService(); diff --git a/frontend/src/types/pipeline.ts b/frontend/src/types/pipeline.ts new file mode 100644 index 00000000..7354aa02 --- /dev/null +++ b/frontend/src/types/pipeline.ts @@ -0,0 +1,199 @@ +/** + * TypeScript types for the analysis pipeline visualization + */ + +// Agent types that perform analysis +export type AgentType = 'market' | 'news' | 'social_media' | 'fundamentals'; + +// Debate types in the system +export type DebateType = 'investment' | 'risk'; + +// Pipeline step status +export type PipelineStepStatus = 'pending' | 'running' | 'completed' | 'error'; + +/** + * Individual agent's analysis report + */ +export interface AgentReport { + agent_type: AgentType; + report_content: string; + data_sources_used: string[]; + created_at?: string; +} + +/** + * Map of agent reports by type + */ +export interface AgentReportsMap { + market?: AgentReport; + news?: AgentReport; + social_media?: AgentReport; + fundamentals?: AgentReport; +} + +/** + * Debate history for investment or risk debates + */ +export interface DebateHistory { + debate_type: DebateType; + // Investment debate fields + bull_arguments?: string; + bear_arguments?: string; + // Risk debate fields + risky_arguments?: string; + safe_arguments?: string; + neutral_arguments?: string; + // Common fields + judge_decision?: string; + full_history?: string; + created_at?: string; +} + +/** + * Map of debates by type + */ +export interface DebatesMap { + investment?: DebateHistory; + risk?: DebateHistory; +} + +/** + * Single step in the analysis pipeline + */ +export interface PipelineStep { + step_number: number; + step_name: string; + status: PipelineStepStatus; + started_at?: string; + completed_at?: string; + duration_ms?: number; + output_summary?: string; +} + +/** + * Log entry for a data source fetch + */ +export interface DataSourceLog { + source_type: string; + source_name: string; + data_fetched?: Record; + fetch_timestamp?: string; + success: boolean; + error_message?: string; +} + +/** + * Complete pipeline data for a single stock analysis + */ +export interface FullPipelineData { + date: string; + symbol: string; + agent_reports: AgentReportsMap; + debates: DebatesMap; + pipeline_steps: PipelineStep[]; + data_sources: DataSourceLog[]; + status?: 'complete' | 'in_progress' | 'no_data'; +} + +/** + * Summary of pipeline for a single stock (used in list views) + */ +export interface PipelineSummary { + symbol: string; + pipeline_steps: { step_name: string; status: PipelineStepStatus }[]; + agent_reports_count: number; + has_debates: boolean; +} + +/** + * API response types + */ +export interface PipelineDataResponse extends FullPipelineData {} + +export interface AgentReportsResponse { + date: string; + symbol: string; + reports: AgentReportsMap; + count: number; +} + +export interface DebateHistoryResponse { + date: string; + symbol: string; + debates: DebatesMap; +} + +export interface DataSourcesResponse { + date: string; + symbol: string; + data_sources: DataSourceLog[]; + count: number; +} + +export interface PipelineSummaryResponse { + date: string; + stocks: PipelineSummary[]; + count: number; +} + +/** + * Pipeline step definitions (for UI rendering) + */ +export const PIPELINE_STEPS = [ + { number: 1, name: 'data_collection', label: 'Data Collection', icon: 'Database' }, + { number: 2, name: 'market_analysis', label: 'Market Analysis', icon: 'TrendingUp' }, + { number: 3, name: 'news_analysis', label: 'News Analysis', icon: 'Newspaper' }, + { number: 4, name: 'social_analysis', label: 'Social Analysis', icon: 'Users' }, + { number: 5, name: 'fundamentals_analysis', label: 'Fundamentals', icon: 'FileText' }, + { number: 6, name: 'investment_debate', label: 'Investment Debate', icon: 'MessageSquare' }, + { number: 7, name: 'trader_decision', label: 'Trader Decision', icon: 'Target' }, + { number: 8, name: 'risk_debate', label: 'Risk Assessment', icon: 'Shield' }, + { number: 9, name: 'final_decision', label: 'Final Decision', icon: 'CheckCircle' }, +] as const; + +/** + * Agent metadata for UI rendering + */ +export const AGENT_METADATA: Record = { + market: { + label: 'Market Analyst', + icon: 'TrendingUp', + color: 'blue', + description: 'Analyzes technical indicators, price trends, and market patterns' + }, + news: { + label: 'News Analyst', + icon: 'Newspaper', + color: 'purple', + description: 'Analyzes company news, macroeconomic trends, and market events' + }, + social_media: { + label: 'Social Media Analyst', + icon: 'Users', + color: 'pink', + description: 'Analyzes social sentiment, Reddit discussions, and public perception' + }, + fundamentals: { + label: 'Fundamentals Analyst', + icon: 'FileText', + color: 'green', + description: 'Analyzes financial statements, ratios, and company health' + } +}; + +/** + * Debate role metadata for UI rendering + */ +export const DEBATE_ROLES = { + investment: { + bull: { label: 'Bull Analyst', color: 'green', icon: 'TrendingUp' }, + bear: { label: 'Bear Analyst', color: 'red', icon: 'TrendingDown' }, + judge: { label: 'Research Manager', color: 'blue', icon: 'Scale' } + }, + risk: { + risky: { label: 'Aggressive Analyst', color: 'red', icon: 'Zap' }, + safe: { label: 'Conservative Analyst', color: 'green', icon: 'Shield' }, + neutral: { label: 'Neutral Analyst', color: 'gray', icon: 'Scale' }, + judge: { label: 'Risk Manager', color: 'blue', icon: 'ShieldCheck' } + } +} as const;