diff --git a/agent_os/backend/routes/runs.py b/agent_os/backend/routes/runs.py index 8d8f9cdc..7c763c08 100644 --- a/agent_os/backend/routes/runs.py +++ b/agent_os/backend/routes/runs.py @@ -104,6 +104,27 @@ async def trigger_auto( background_tasks.add_task(_run_and_store, run_id, engine.run_auto(run_id, params or {})) return {"run_id": run_id, "status": "queued"} +@router.delete("/portfolio-stage") +async def reset_portfolio_stage( + params: Dict[str, Any], + user: dict = Depends(get_current_user), +): + """Delete PM decision and execution result for a given date/portfolio_id. + + After calling this, an auto run will re-run Phase 3 from scratch + (Phases 1 & 2 are skipped if their cached results still exist). + """ + from tradingagents.portfolio.report_store import ReportStore + date = params.get("date") + portfolio_id = params.get("portfolio_id") + if not date or not portfolio_id: + raise HTTPException(status_code=422, detail="date and portfolio_id are required") + store = ReportStore() + deleted = store.clear_portfolio_stage(date, portfolio_id) + logger.info("reset_portfolio_stage date=%s portfolio=%s deleted=%s user=%s", date, portfolio_id, deleted, user["user_id"]) + return {"deleted": deleted, "date": date, "portfolio_id": portfolio_id} + + @router.get("/") async def list_runs(user: dict = Depends(get_current_user)): # Filter by user in production diff --git a/agent_os/frontend/src/Dashboard.tsx b/agent_os/frontend/src/Dashboard.tsx index 0f794c26..80317f14 100644 --- a/agent_os/frontend/src/Dashboard.tsx +++ b/agent_os/frontend/src/Dashboard.tsx @@ -35,7 +35,7 @@ import { Collapse, useToast, } from '@chakra-ui/react'; -import { LayoutDashboard, Wallet, Settings, Terminal as TerminalIcon, ChevronRight, Eye, Search, BarChart3, Bot, ChevronDown, ChevronUp } from 'lucide-react'; +import { LayoutDashboard, Wallet, Settings, Terminal as TerminalIcon, ChevronRight, Eye, Search, BarChart3, Bot, ChevronDown, ChevronUp, Trash2 } from 'lucide-react'; import { MetricHeader } from './components/MetricHeader'; import { AgentGraph } from './components/AgentGraph'; import { PortfolioViewer } from './components/PortfolioViewer'; @@ -371,6 +371,27 @@ export const Dashboard: React.FC = () => { } }; + const resetPortfolioStage = async () => { + if (!params.date || !params.portfolio_id) { + toast({ title: 'Date and Portfolio ID are required', status: 'warning', duration: 3000, isClosable: true, position: 'top' }); + setShowParams(true); + return; + } + try { + const res = await axios.delete(`${API_BASE}/run/portfolio-stage`, { data: { date: params.date, portfolio_id: params.portfolio_id } }); + const deleted: string[] = res.data.deleted; + toast({ + title: deleted.length ? `Cleared: ${deleted.join(', ')}` : 'Nothing to clear — no decision files found', + status: deleted.length ? 'success' : 'info', + duration: 4000, + isClosable: true, + position: 'top', + }); + } catch (err) { + toast({ title: 'Failed to reset portfolio stage', status: 'error', duration: 3000, isClosable: true, position: 'top' }); + } + }; + /** Open the full-screen event detail modal */ const openModal = useCallback((evt: AgentEvent) => { setModalEvent(evt); @@ -483,6 +504,19 @@ export const Dashboard: React.FC = () => { ); })} + + + + {status.toUpperCase()} diff --git a/tradingagents/portfolio/report_store.py b/tradingagents/portfolio/report_store.py index e40c32d8..9ee27401 100644 --- a/tradingagents/portfolio/report_store.py +++ b/tradingagents/portfolio/report_store.py @@ -303,6 +303,23 @@ class ReportStore: path = self._portfolio_dir(date) / f"{portfolio_id}_execution_result.json" return self._read_json(path) + def clear_portfolio_stage(self, date: str, portfolio_id: str) -> list[str]: + """Delete PM decision and execution result files for a given date/portfolio. + + Returns a list of deleted file names so the caller can log what was removed. + """ + targets = [ + self._portfolio_dir(date) / f"{portfolio_id}_pm_decision.json", + self._portfolio_dir(date) / f"{portfolio_id}_pm_decision.md", + self._portfolio_dir(date) / f"{portfolio_id}_execution_result.json", + ] + deleted = [] + for path in targets: + if path.exists(): + path.unlink() + deleted.append(path.name) + return deleted + def list_pm_decisions(self, portfolio_id: str) -> list[Path]: """Return all saved PM decision JSON paths for portfolio_id, newest first.