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:
parent
2a17ea0cca
commit
860968835c
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue