feat: add Reset Decision button to clear portfolio stage and re-run

- ReportStore.clear_portfolio_stage(date, portfolio_id): deletes pm_decision
  (.json + .md) and execution_result files for a given date/portfolio
- DELETE /api/run/portfolio-stage endpoint: calls clear_portfolio_stage
  and returns list of deleted files
- Dashboard: 'Reset Decision' button calls the endpoint, then user can
  run Auto to re-run Phase 3 from scratch while skipping Phase 1 & 2

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Ahmet Guzererler 2026-03-24 00:34:53 +01:00
parent 2a17ea0cca
commit 860968835c
3 changed files with 73 additions and 1 deletions

View File

@ -104,6 +104,27 @@ async def trigger_auto(
background_tasks.add_task(_run_and_store, run_id, engine.run_auto(run_id, params or {})) background_tasks.add_task(_run_and_store, run_id, engine.run_auto(run_id, params or {}))
return {"run_id": run_id, "status": "queued"} 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("/") @router.get("/")
async def list_runs(user: dict = Depends(get_current_user)): async def list_runs(user: dict = Depends(get_current_user)):
# Filter by user in production # Filter by user in production

View File

@ -35,7 +35,7 @@ import {
Collapse, Collapse,
useToast, useToast,
} from '@chakra-ui/react'; } 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 { MetricHeader } from './components/MetricHeader';
import { AgentGraph } from './components/AgentGraph'; import { AgentGraph } from './components/AgentGraph';
import { PortfolioViewer } from './components/PortfolioViewer'; 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 */ /** Open the full-screen event detail modal */
const openModal = useCallback((evt: AgentEvent) => { const openModal = useCallback((evt: AgentEvent) => {
setModalEvent(evt); setModalEvent(evt);
@ -483,6 +504,19 @@ export const Dashboard: React.FC = () => {
); );
})} })}
<Divider orientation="vertical" h="20px" /> <Divider orientation="vertical" h="20px" />
<Tooltip label="Clear PM decision & execution result for this date/portfolio, then re-run Auto to start Phase 3 fresh">
<Button
size="sm"
leftIcon={<Trash2 size={14} />}
colorScheme="red"
variant="outline"
onClick={resetPortfolioStage}
isDisabled={isRunning}
>
Reset Decision
</Button>
</Tooltip>
<Divider orientation="vertical" h="20px" />
<Tag size="sm" colorScheme={status === 'streaming' ? 'green' : status === 'completed' ? 'blue' : status === 'error' ? 'red' : 'gray'}> <Tag size="sm" colorScheme={status === 'streaming' ? 'green' : status === 'completed' ? 'blue' : status === 'error' ? 'red' : 'gray'}>
{status.toUpperCase()} {status.toUpperCase()}
</Tag> </Tag>

View File

@ -303,6 +303,23 @@ class ReportStore:
path = self._portfolio_dir(date) / f"{portfolio_id}_execution_result.json" path = self._portfolio_dir(date) / f"{portfolio_id}_execution_result.json"
return self._read_json(path) 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]: def list_pm_decisions(self, portfolio_id: str) -> list[Path]:
"""Return all saved PM decision JSON paths for portfolio_id, newest first. """Return all saved PM decision JSON paths for portfolio_id, newest first.