Skip to content

Commit 1a3a614

Browse files
committed
feat: 引入上下文记忆与能力分层 Agent 架构
- 新增 agent workflow/capability 分层与种子规范,限制 capability 参与调度与股票绑定\n- 增加版本化数据库迁移与迁移前备份,扩展 AgentRun/AnalysisHistory 观测字段\n- 新增上下文快照、新闻主题快照、上下文运行记录与后验评估落库/清理链路\n- 盘前/盘中/收盘 Agent 接入分层新闻、历史K线、资金约束与质量评分,并写入历史调试信息\n- 前端历史与洞察页面支持 kind 筛选、按 updated_at 排序及上下文/Prompt 调试展示\n\nValidation:\n- python -m py_compile server.py src/agents/daily_report.py src/agents/intraday_monitor.py src/agents/premarket_outlook.py src/core/agent_catalog.py src/core/context_builder.py src/core/context_scheduler.py src/core/context_store.py src/core/json_safe.py src/core/kline_context.py src/core/news_ranker.py src/core/prediction_outcome.py src/web/api/agents.py src/web/api/history.py src/web/api/stocks.py src/web/api/templates.py src/web/api/context.py src/web/database.py src/web/migrations.py src/web/models.py src/web/app.py
1 parent 174d468 commit 1a3a614

32 files changed

Lines changed: 3518 additions & 154 deletions

frontend/packages/biz-ui/src/components/stock-insight-modal.tsx

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,14 @@ interface HistoryRecord {
7878
publish_time?: string
7979
url?: string
8080
}> | null
81+
quality_overview?: Record<string, any> | null
82+
context_summary?: Record<string, any> | null
83+
context_payload?: Record<string, any> | null
84+
prompt_context?: string | null
85+
prompt_stats?: Record<string, any> | null
86+
news_debug?: Record<string, any> | null
8187
created_at: string
88+
updated_at?: string
8289
}
8390

8491
interface PortfolioPosition {
@@ -628,8 +635,8 @@ export default function StockInsightModal(props: {
628635
.filter(Boolean) as HistoryRecord[]
629636
}
630637
merged = merged.sort((a, b) => {
631-
const am = parseToMs(a.created_at || a.analysis_date) || 0
632-
const bm = parseToMs(b.created_at || b.analysis_date) || 0
638+
const am = parseToMs(a.updated_at || a.created_at || a.analysis_date) || 0
639+
const bm = parseToMs(b.updated_at || b.created_at || b.analysis_date) || 0
633640
return bm - am
634641
})
635642
setReports(merged)
@@ -1537,6 +1544,35 @@ export default function StockInsightModal(props: {
15371544
<ReactMarkdown>{activeReport.content || '暂无报告内容'}</ReactMarkdown>
15381545
</div>
15391546
</div>
1547+
{(activeReport.prompt_context || activeReport.context_payload || activeReport.news_debug) && (
1548+
<details className="rounded-lg border border-border/40 bg-accent/10 p-3">
1549+
<summary className="cursor-pointer text-[12px] text-muted-foreground select-none">查看分析上下文</summary>
1550+
{activeReport.prompt_stats ? (
1551+
<div className="mt-2">
1552+
<div className="text-[11px] text-muted-foreground mb-1">Prompt统计</div>
1553+
<pre className="text-[11px] text-muted-foreground whitespace-pre-wrap break-words overflow-x-auto">{JSON.stringify(activeReport.prompt_stats, null, 2)}</pre>
1554+
</div>
1555+
) : null}
1556+
{activeReport.news_debug ? (
1557+
<div className="mt-2">
1558+
<div className="text-[11px] text-muted-foreground mb-1">新闻注入明细</div>
1559+
<pre className="text-[11px] text-muted-foreground whitespace-pre-wrap break-words overflow-x-auto">{JSON.stringify(activeReport.news_debug, null, 2)}</pre>
1560+
</div>
1561+
) : null}
1562+
{activeReport.context_payload ? (
1563+
<div className="mt-2">
1564+
<div className="text-[11px] text-muted-foreground mb-1">上下文快照</div>
1565+
<pre className="text-[11px] text-muted-foreground whitespace-pre-wrap break-words overflow-x-auto max-h-[220px] overflow-y-auto">{JSON.stringify(activeReport.context_payload, null, 2)}</pre>
1566+
</div>
1567+
) : null}
1568+
{activeReport.prompt_context ? (
1569+
<div className="mt-2">
1570+
<div className="text-[11px] text-muted-foreground mb-1">Prompt原文</div>
1571+
<pre className="text-[11px] text-muted-foreground whitespace-pre-wrap break-words overflow-x-auto max-h-[220px] overflow-y-auto">{activeReport.prompt_context}</pre>
1572+
</div>
1573+
) : null}
1574+
</details>
1575+
)}
15401576
</div>
15411577
)}
15421578
</div>

frontend/src/lib/logger-map.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Map Python module logger names to concise Chinese display names
22
export const LOGGER_MAPPING: Record<string, string> = {
33
// Agents
4-
'src.agents.daily_report': '盘后日报',
4+
'src.agents.daily_report': '收盘复盘',
55
'src.agents.premarket_outlook': '盘前分析',
66
'src.agents.intraday_monitor': '盘中监测',
77
'src.agents.news_digest': '新闻速递',

frontend/src/pages/Agents.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -392,8 +392,8 @@ export default function AgentsPage() {
392392
const triggerAgent = async (name: string) => {
393393
setTriggering(name)
394394
try {
395-
await fetchAPI(`/agents/${name}/trigger`, { method: 'POST' })
396-
toast('Agent 已触发', 'success')
395+
const res = await fetchAPI<{ queued?: boolean; message?: string }>(`/agents/${name}/trigger`, { method: 'POST' })
396+
toast(res?.queued ? 'Agent 已提交后台执行' : (res?.message || 'Agent 已触发'), 'success')
397397
} catch (e) {
398398
toast(e instanceof Error ? e.message : '触发失败', 'error')
399399
} finally {

frontend/src/pages/Dashboard.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -481,7 +481,7 @@ export default function DashboardPage() {
481481
const [dailyData, premarketData, newsData] = await Promise.all([
482482
fetchAPI<AnalysisRecord[]>('/history?agent_name=daily_report&limit=1'),
483483
fetchAPI<AnalysisRecord[]>('/history?agent_name=premarket_outlook&limit=1'),
484-
fetchAPI<AnalysisRecord[]>('/history?agent_name=news_digest&limit=1'),
484+
fetchAPI<AnalysisRecord[]>('/history?agent_name=news_digest&kind=all&limit=1'),
485485
])
486486
setDailyReport(dailyData.length > 0 ? dailyData[0] : null)
487487
setPremarketOutlook(premarketData.length > 0 ? premarketData[0] : null)
@@ -725,7 +725,7 @@ export default function DashboardPage() {
725725

726726
const insightCards = useMemo(() => {
727727
const cards = [
728-
{ key: 'daily', title: '盘后日报', icon: Moon, style: 'bg-orange-500/10 text-orange-500', record: dailyReport },
728+
{ key: 'daily', title: '收盘复盘', icon: Moon, style: 'bg-orange-500/10 text-orange-500', record: dailyReport },
729729
{ key: 'premarket', title: '盘前分析', icon: Sun, style: 'bg-amber-500/10 text-amber-500', record: premarketOutlook },
730730
{ key: 'news', title: '新闻速递', icon: Newspaper, style: 'bg-blue-500/10 text-blue-500', record: newsDigest },
731731
]

frontend/src/pages/History.tsx

Lines changed: 110 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,36 +11,79 @@ import { useToast } from '@panwatch/base-ui/components/ui/toast'
1111
interface HistoryRecord {
1212
id: number
1313
agent_name: string
14+
agent_kind?: 'workflow' | 'capability'
1415
stock_symbol: string
1516
analysis_date: string
1617
title: string
1718
content: string
19+
context_payload?: Record<string, unknown> | null
20+
prompt_context?: string | null
21+
prompt_stats?: Record<string, unknown> | null
22+
news_debug?: Record<string, unknown> | null
1823
created_at: string
1924
updated_at: string
2025
}
2126

2227
const AGENT_LABELS: Record<string, string> = {
23-
daily_report: '盘后日报',
28+
daily_report: '收盘复盘',
2429
premarket_outlook: '盘前分析',
2530
intraday_monitor: '盘中监测',
2631
news_digest: '新闻速递',
2732
chart_analyst: '技术分析',
2833
}
2934

35+
const WORKFLOW_AGENT_KEYS = ['daily_report', 'premarket_outlook', 'intraday_monitor']
36+
const CAPABILITY_AGENT_KEYS = ['news_digest', 'chart_analyst']
37+
3038
export default function HistoryPage() {
3139
const { toast } = useToast()
3240
const [records, setRecords] = useState<HistoryRecord[]>([])
3341
const [loading, setLoading] = useState(true)
3442
const [selectedAgent, setSelectedAgent] = useState<string>('all')
43+
const [historyKind, setHistoryKind] = useState<'workflow' | 'capability' | 'all'>('workflow')
3544
const [selectedId, setSelectedId] = useState<number | null>(null)
3645
const [mobileView, setMobileView] = useState<'list' | 'reader'>('list')
3746
const [detailRecord, setDetailRecord] = useState<HistoryRecord | null>(null)
3847

48+
const displayTime = (record: HistoryRecord) => record.updated_at || record.created_at
49+
const formatDateTime = (iso?: string) => {
50+
if (!iso) return '--'
51+
const s = String(iso).trim()
52+
if (!s) return '--'
53+
// Keep original offset semantics; only normalize display format and strip fractional seconds.
54+
let normalized = s.replace(' ', 'T').replace(/Z$/, '+00:00')
55+
normalized = normalized.replace(/\.\d+(?=[+-]\d{2}:\d{2}$)/, '')
56+
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[+-]\d{2}:\d{2}$/.test(normalized)) {
57+
return normalized
58+
}
59+
const d = new Date(s)
60+
if (isNaN(d.getTime())) return s
61+
const pad = (n: number) => String(n).padStart(2, '0')
62+
const year = d.getFullYear()
63+
const month = pad(d.getMonth() + 1)
64+
const day = pad(d.getDate())
65+
const hour = pad(d.getHours())
66+
const minute = pad(d.getMinutes())
67+
const second = pad(d.getSeconds())
68+
const tz = -d.getTimezoneOffset()
69+
const sign = tz >= 0 ? '+' : '-'
70+
const tzHour = pad(Math.floor(Math.abs(tz) / 60))
71+
const tzMinute = pad(Math.abs(tz) % 60)
72+
return `${year}-${month}-${day}T${hour}:${minute}:${second}${sign}${tzHour}:${tzMinute}`
73+
}
74+
75+
const formatTimeShort = (iso?: string) => {
76+
const full = formatDateTime(iso)
77+
const m = full.match(/T(\d{2}:\d{2}):\d{2}[+-]\d{2}:\d{2}$/)
78+
return m ? m[1] : '--:--'
79+
}
80+
3981
const load = async () => {
4082
setLoading(true)
4183
try {
4284
const params = new URLSearchParams()
4385
if (selectedAgent && selectedAgent !== 'all') params.set('agent_name', selectedAgent)
86+
params.set('kind', historyKind)
4487
params.set('limit', '50')
4588
const data = await fetchAPI<HistoryRecord[]>(`/history?${params.toString()}`)
4689
setRecords(data || [])
@@ -51,7 +94,18 @@ export default function HistoryPage() {
5194
}
5295
}
5396

54-
useEffect(() => { load() }, [selectedAgent])
97+
useEffect(() => { load() }, [selectedAgent, historyKind])
98+
99+
useEffect(() => {
100+
const available = historyKind === 'workflow'
101+
? WORKFLOW_AGENT_KEYS
102+
: historyKind === 'capability'
103+
? CAPABILITY_AGENT_KEYS
104+
: [...WORKFLOW_AGENT_KEYS, ...CAPABILITY_AGENT_KEYS]
105+
if (selectedAgent !== 'all' && !available.includes(selectedAgent)) {
106+
setSelectedAgent('all')
107+
}
108+
}, [historyKind, selectedAgent])
55109

56110
useEffect(() => {
57111
if (!records.length) {
@@ -84,6 +138,11 @@ export default function HistoryPage() {
84138
}
85139

86140
const selectedRecord = selectedId ? records.find(r => r.id === selectedId) || null : null
141+
const agentOptions = historyKind === 'workflow'
142+
? WORKFLOW_AGENT_KEYS
143+
: historyKind === 'capability'
144+
? CAPABILITY_AGENT_KEYS
145+
: [...WORKFLOW_AGENT_KEYS, ...CAPABILITY_AGENT_KEYS]
87146

88147
const selectRecord = (id: number) => {
89148
setSelectedId(id)
@@ -112,17 +171,29 @@ export default function HistoryPage() {
112171
</div>
113172
</div>
114173

115-
<Select value={selectedAgent} onValueChange={setSelectedAgent}>
116-
<SelectTrigger className="w-full sm:w-[180px] h-9">
117-
<SelectValue placeholder="全部 Agent" />
118-
</SelectTrigger>
119-
<SelectContent>
120-
<SelectItem value="all">全部 Agent</SelectItem>
121-
{Object.entries(AGENT_LABELS).map(([key, label]) => (
122-
<SelectItem key={key} value={key}>{label}</SelectItem>
123-
))}
124-
</SelectContent>
125-
</Select>
174+
<div className="flex items-center gap-2">
175+
<Select value={historyKind} onValueChange={(v) => setHistoryKind(v as 'workflow' | 'capability' | 'all')}>
176+
<SelectTrigger className="w-full sm:w-[150px] h-9">
177+
<SelectValue placeholder="历史范围" />
178+
</SelectTrigger>
179+
<SelectContent>
180+
<SelectItem value="workflow">主流程</SelectItem>
181+
<SelectItem value="capability">能力层</SelectItem>
182+
<SelectItem value="all">全部</SelectItem>
183+
</SelectContent>
184+
</Select>
185+
<Select value={selectedAgent} onValueChange={setSelectedAgent}>
186+
<SelectTrigger className="w-full sm:w-[180px] h-9">
187+
<SelectValue placeholder="全部 Agent" />
188+
</SelectTrigger>
189+
<SelectContent>
190+
<SelectItem value="all">全部 Agent</SelectItem>
191+
{agentOptions.map((key) => (
192+
<SelectItem key={key} value={key}>{AGENT_LABELS[key] || key}</SelectItem>
193+
))}
194+
</SelectContent>
195+
</Select>
196+
</div>
126197
</div>
127198

128199
{loading ? (
@@ -177,7 +248,7 @@ export default function HistoryPage() {
177248
</div>
178249
<div className="mt-1 flex items-center justify-between text-[11px] text-muted-foreground">
179250
<span className="font-mono">{r.analysis_date}</span>
180-
<span>{new Date(r.created_at).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })}</span>
251+
<span>{formatTimeShort(displayTime(r))}</span>
181252
</div>
182253
</button>
183254
)
@@ -202,7 +273,7 @@ export default function HistoryPage() {
202273
目录
203274
</Button>
204275
<Badge variant="outline" className="text-[10px]">{AGENT_LABELS[selectedRecord.agent_name] || selectedRecord.agent_name}</Badge>
205-
<span className="text-[11px] text-muted-foreground font-mono">{selectedRecord.created_at}</span>
276+
<span className="text-[11px] text-muted-foreground font-mono">{formatDateTime(displayTime(selectedRecord))}</span>
206277
</div>
207278
<div className="mt-1 text-[15px] md:text-[16px] font-semibold text-foreground truncate">
208279
{formatTitle(selectedRecord)}
@@ -249,6 +320,30 @@ export default function HistoryPage() {
249320
<div className="mt-4 p-4 bg-accent/20 rounded-lg prose prose-sm dark:prose-invert max-w-none">
250321
{detailRecord && <ReactMarkdown>{detailRecord.content}</ReactMarkdown>}
251322
</div>
323+
{detailRecord?.prompt_stats ? (
324+
<div className="mt-3 rounded-lg border border-border/50 p-3">
325+
<div className="text-[12px] font-medium mb-1">Prompt 统计</div>
326+
<pre className="text-[11px] text-muted-foreground whitespace-pre-wrap break-words overflow-x-auto">{JSON.stringify(detailRecord.prompt_stats, null, 2)}</pre>
327+
</div>
328+
) : null}
329+
{detailRecord?.context_payload ? (
330+
<div className="mt-3 rounded-lg border border-border/50 p-3">
331+
<div className="text-[12px] font-medium mb-1">上下文快照</div>
332+
<pre className="text-[11px] text-muted-foreground whitespace-pre-wrap break-words overflow-x-auto max-h-[280px] overflow-y-auto">{JSON.stringify(detailRecord.context_payload, null, 2)}</pre>
333+
</div>
334+
) : null}
335+
{detailRecord?.news_debug ? (
336+
<div className="mt-3 rounded-lg border border-border/50 p-3">
337+
<div className="text-[12px] font-medium mb-1">新闻注入明细</div>
338+
<pre className="text-[11px] text-muted-foreground whitespace-pre-wrap break-words overflow-x-auto">{JSON.stringify(detailRecord.news_debug, null, 2)}</pre>
339+
</div>
340+
) : null}
341+
{detailRecord?.prompt_context ? (
342+
<div className="mt-3 rounded-lg border border-border/50 p-3">
343+
<div className="text-[12px] font-medium mb-1">Prompt 原文</div>
344+
<pre className="text-[11px] text-muted-foreground whitespace-pre-wrap break-words overflow-x-auto max-h-[280px] overflow-y-auto">{detailRecord.prompt_context}</pre>
345+
</div>
346+
) : null}
252347
</DialogContent>
253348
</Dialog>
254349
</div>

frontend/src/pages/Settings.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,6 @@ export default function SettingsPage() {
196196
agents: [
197197
{ name: 'premarket_outlook', enabled: true, schedule: '30 8 * * 1-5', execution_mode: 'batch' },
198198
{ name: 'daily_report', enabled: true, schedule: '30 15 * * 1-5', execution_mode: 'batch' },
199-
{ name: 'news_digest', enabled: true, schedule: '0 */3 * * 1-5', execution_mode: 'batch' },
200199
{ name: 'intraday_monitor', enabled: true, schedule: '*/10 9-15 * * 1-5', execution_mode: 'single', config: { event_only: true, price_alert_threshold: 4.0, volume_alert_ratio: 2.5, throttle_minutes: 45 } },
201200
],
202201
},
@@ -213,7 +212,6 @@ export default function SettingsPage() {
213212
agents: [
214213
{ name: 'premarket_outlook', enabled: true, schedule: '30 8 * * 1-5', execution_mode: 'batch' },
215214
{ name: 'daily_report', enabled: true, schedule: '30 15 * * 1-5', execution_mode: 'batch' },
216-
{ name: 'news_digest', enabled: true, schedule: '0 */2 * * 1-5', execution_mode: 'batch' },
217215
{ name: 'intraday_monitor', enabled: true, schedule: '*/5 9-15 * * 1-5', execution_mode: 'single', config: { event_only: true, price_alert_threshold: 3.0, volume_alert_ratio: 2.0, throttle_minutes: 30 } },
218216
],
219217
},
@@ -230,7 +228,6 @@ export default function SettingsPage() {
230228
agents: [
231229
{ name: 'premarket_outlook', enabled: true, schedule: '10 8 * * 1-5', execution_mode: 'batch' },
232230
{ name: 'daily_report', enabled: true, schedule: '10 15 * * 1-5', execution_mode: 'batch' },
233-
{ name: 'news_digest', enabled: true, schedule: '0 * * * 1-5', execution_mode: 'batch' },
234231
{ name: 'intraday_monitor', enabled: true, schedule: '*/3 9-15 * * 1-5', execution_mode: 'single', config: { event_only: true, price_alert_threshold: 2.0, volume_alert_ratio: 1.8, throttle_minutes: 20 } },
235232
],
236233
},

0 commit comments

Comments
 (0)