diff --git a/backend/src/services/apiServer.ts b/backend/src/services/apiServer.ts index dd982bd..c367c03 100644 --- a/backend/src/services/apiServer.ts +++ b/backend/src/services/apiServer.ts @@ -363,6 +363,7 @@ interface ChatResponsePayload { profile?: ChatProfilePayload; summary: string; reasoning: string; + insights?: string[]; nextActions?: string[]; quickLinks?: ChatQuickLink[]; fallback?: 'local_deterministic'; @@ -1112,6 +1113,11 @@ export class ApiServer { action: 'explain_position', summary: `${target.symbol} is currently ${pnl >= 0 ? 'up' : 'down'} ${Math.abs(pnl).toFixed(2)} (${Math.abs(pnlPercent).toFixed(2)}%) under ${profileName}.`, reasoning: `${nextStep}${targetHints.length > 0 ? ` Current guardrails: ${targetHints.join(' and ')}.` : ''} Entry ${Number(target.entryPrice || 0).toFixed(2)}, current ${Number(target.currentPrice || 0).toFixed(2)}, size ${Number(target.size || 0).toFixed(6)}.`, + insights: [ + `Position side: ${side}`, + `Entry price: ${Number(target.entryPrice || 0).toFixed(2)}`, + `Current price: ${Number(target.currentPrice || 0).toFixed(2)}`, + ], nextActions: [ 'Open Trade Plans if you want to attach, pause, or resume automated exits.', 'Review Portfolio for the linked holding and current P&L before changing the plan.', @@ -1169,6 +1175,11 @@ export class ApiServer { reasoning: healthNotes.length > 0 ? `Operational context also shows ${healthNotes.join(', ')}. The safest next step is to review the related profile, live order state, and reconciliation surfaces before retrying execution.` : 'The safest next step is to review the related profile, live order state, and recent operational events before retrying execution.', + insights: [ + recentFailure?.tradeId ? `Trade ID: ${recentFailure.tradeId}` : null, + recentFailure?.reason ? `Latest failure: ${recentFailure.reason}` : null, + recentEvent?.message ? `Latest operational event: ${recentEvent.message}` : null, + ].filter(Boolean) as string[], nextActions: [ 'Check the latest operational event and order failure reason for the affected symbol.', 'If the blocker is persistent, inspect the Admin reconciliation surfaces before retrying.', @@ -1202,6 +1213,11 @@ export class ApiServer { reasoning: target.executionCode ? `Execution code: ${target.executionCode}. This means the bot evaluated the setup but did not proceed yet.${target.orderId ? ` Related order: ${target.orderId}.` : ''}` : 'The bot is still evaluating rule alignment, risk constraints, or session eligibility before entering.', + insights: [ + `Execution status: ${executionStatus}`, + target.executionCode ? `Execution code: ${target.executionCode}` : null, + target.orderId ? `Related order: ${target.orderId}` : null, + ].filter(Boolean) as string[], nextActions: [ 'Review the active profile rules and recent symbol signal panel to see which rule or guardrail is not aligned.', 'If the waiting state persists, ask the copilot to recommend a profile change for this setup.', @@ -1236,6 +1252,11 @@ export class ApiServer { reasoning: degraded ? `Missing-from-exchange: ${missingExchange}. Missing-in-DB: ${missingDb}. Auto-closed parity trades: ${autoClosedCount}.${recentReconEvents.length > 0 ? ` Recent events: ${recentReconEvents.join(' | ')}` : ''}` : `Auto-closed parity trades this cycle: ${autoClosedCount}.${recentReconEvents.length > 0 ? ` Recent events: ${recentReconEvents.join(' | ')}` : ''}`, + insights: [ + `Missing from exchange: ${missingExchange}`, + `Missing in DB: ${missingDb}`, + `Auto-closed parity trades: ${autoClosedCount}`, + ], nextActions: degraded ? [ 'Open the Admin reconciliation audit to inspect mismatches, NO_GO trades, and any quarantined rows.', @@ -1302,6 +1323,11 @@ export class ApiServer { reasoning: waitingSignal?.executionCode === 'rule_ratio_not_met' ? `Recent signal context shows ${waitingSignal.symbol} is waiting on rule alignment. I reduced cooldown pressure and slightly tightened exposure so the profile can recycle opportunities more cleanly.` : 'I preserved the existing strategy structure and adjusted only the higher-leverage controls such as risk per trade, cooldown, and open-trade exposure.', + insights: [ + `Suggested risk/trade: ${recommendedProfile.risk_per_trade_percent}%`, + `Suggested max open trades: ${recommendedProfile.strategy_config?.riskLimits?.maxOpenTrades ?? 'n/a'}`, + `Suggested cooldown: ${recommendedProfile.strategy_config?.execution?.cooldownMinutes ?? 'n/a'} min`, + ], nextActions: [ 'Review the suggested profile draft below before applying it.', 'After applying, monitor the next signal cycle to see whether the waiting or blocker state improves.', @@ -1323,6 +1349,11 @@ export class ApiServer { action: 'recommend_trade_plan', summary: `A Trade Plan would help manage ${position.symbol} more explicitly from here.`, reasoning: `You already have a live holding on ${position.symbol}. The safest automation step is to open Plans and attach or refine an exit plan tied to trade ${position.tradeId || 'the current holding'}.`, + insights: [ + `Live holding: ${position.symbol}`, + position.tradeId ? `Trade ID: ${position.tradeId}` : null, + Number.isFinite(Number(position.unrealizedPnl)) ? `Current unrealized P&L: ${Number(position.unrealizedPnl || 0).toFixed(2)}` : null, + ].filter(Boolean) as string[], nextActions: [ 'Open Plans to attach or adjust a profit exit for this holding.', 'If you want to stop automation, convert the position to a long-term hold in Plans.', @@ -1346,6 +1377,12 @@ export class ApiServer { action: 'review_recent_trades', summary: `Your last ${trades.length} closed trades show ${winners} winners, ${losers} losers, and net P&L of ${totalPnl.toFixed(2)}.${topReason}`, reasoning: 'Use the most recent closed trades as the fastest feedback loop for whether your current profile settings and Trade Plans are producing the outcomes you expect.', + insights: trades.slice(0, 3).map((trade) => { + const symbol = String(trade.symbol || 'unknown'); + const pnl = Number(trade.pnl || 0).toFixed(2); + const reason = trade.reason ? ` (${trade.reason})` : ''; + return `${symbol}: ${pnl}${reason}`; + }), nextActions: [ 'Open Portfolio trade history to inspect the latest winners and losers in detail.', 'If one profile is underperforming repeatedly, ask the copilot to recommend a profile change for it.', @@ -1365,6 +1402,11 @@ export class ApiServer { action: 'recommend_reconciliation_followup', summary: `The safest next remediation step is to inspect reconciliation before forcing more execution. Current counts: mismatches=${mismatchCount}, no_go=${noGoCount}, quarantined=${quarantinedCount}.`, reasoning: 'When reconciliation is degraded, fixing the underlying mismatch is safer than retrying trades blindly because it prevents lifecycle drift and duplicate execution.', + insights: [ + `Mismatch count: ${mismatchCount}`, + `NO_GO trades: ${noGoCount}`, + `Quarantined trades: ${quarantinedCount}`, + ], nextActions: [ 'Open the Admin reconciliation panel and inspect the newest mismatches first.', 'Use the affected symbol and profile from the latest operational events to narrow the problem quickly.', @@ -3223,6 +3265,7 @@ PROFILE SCHEMA: }, "summary": string (1-2 sentence human-readable summary of what you did), "reasoning": string (brief explanation of why you chose these parameters or what operational state means), + "insights": string[] (optional concise evidence or key facts), "nextActions": string[] (optional safe next steps for the user), "quickLinks": [{ kind: "portfolio" | "plans" | "settings", ... }] (optional safe in-app destinations) } @@ -3247,7 +3290,7 @@ RULES: 11. For "summarize_reconciliation": summarize reconciliation health, mismatches, quarantines, and operational implications. No profile needed. 12. Only include "profile" when the action is create_profile, update_profile, or recommend_profile_change. 13. If runtime context is relevant, prefer concrete symbols, profile names, counts, and current state over generic advice. -14. Include safe "nextActions" and "quickLinks" whenever they would help the user move to the right app surface. +14. Include safe "insights", "nextActions", and "quickLinks" whenever they would help the user move to the right app surface. 15. Match the user's risk appetite: "conservative" = low risk (0.5-1%), low capital. "aggressive" = higher risk (2-5%), more rules enabled. 16. Always include at least TrendBiasRule and RiskManagementRule as enabled for safety. 17. Output ONLY valid JSON. No markdown, no backticks, no explanation outside the JSON.`; diff --git a/backend/verifyChatCopilotContract.ts b/backend/verifyChatCopilotContract.ts index fd1c05d..032396f 100644 --- a/backend/verifyChatCopilotContract.ts +++ b/backend/verifyChatCopilotContract.ts @@ -55,13 +55,17 @@ function testChatActionContract() { } function testQuickLinkContract() { + assertSourceIncludes( + `"insights": string[] (optional concise evidence or key facts)`, + 'chat prompt contract must document structured insights', + ); assertSourceIncludes( `"quickLinks": [{ kind: "portfolio" | "plans" | "settings", ... }] (optional safe in-app destinations)`, 'chat prompt contract must document quickLinks', ); assertSourceIncludes( - 'Include safe "nextActions" and "quickLinks" whenever they would help the user move to the right app surface.', - 'chat prompt rules must require safe nextActions/quickLinks guidance', + 'Include safe "insights", "nextActions", and "quickLinks" whenever they would help the user move to the right app surface.', + 'chat prompt rules must require safe insights/nextActions/quickLinks guidance', ); assertSourceIncludes( "{ kind: 'plans', label: 'Manage in Plans'", diff --git a/web/src/components/ChatControl.dom.test.tsx b/web/src/components/ChatControl.dom.test.tsx index 88e8fee..3375ebc 100644 --- a/web/src/components/ChatControl.dom.test.tsx +++ b/web/src/components/ChatControl.dom.test.tsx @@ -115,7 +115,7 @@ describe('ChatControl DOM flow', () => { await user.type(screen.getByPlaceholderText(/Ask for a profile, holding explanation, or reconciliation help/i), 'Create a conservative profile{enter}'); await waitFor(() => { - expect(screen.getByText(/Error: Not authenticated/i)).toBeInTheDocument(); + expect(screen.getByText(/session is not authorized for chat right now/i)).toBeInTheDocument(); }, { timeout: 3000 }); }, 15000); @@ -221,6 +221,7 @@ describe('ChatControl DOM flow', () => { summary: 'BTC/USDT is currently up 375.00 (2.50%) under High Risk Scalper ⚡.', reasoning: 'The bot is primarily waiting for either your profit target, stop-loss, or a management change from you.', action: 'explain_position', + insights: ['Position side: BUY', 'Entry price: 60000.00'], }) } as any); @@ -233,6 +234,8 @@ describe('ChatControl DOM flow', () => { await waitFor(() => { expect(screen.getByText(/BTC\/USDT is currently up 375.00/i)).toBeInTheDocument(); expect(screen.getByText(/waiting for either your profit target/i)).toBeInTheDocument(); + expect(screen.getByText(/Key facts/i)).toBeInTheDocument(); + expect(screen.getByText(/Position side: BUY/i)).toBeInTheDocument(); }); expect(screen.queryByRole('button', { name: /Apply to Dashboard/i })).not.toBeInTheDocument(); diff --git a/web/src/components/ChatControl.tsx b/web/src/components/ChatControl.tsx index 037d55b..34ea06d 100644 --- a/web/src/components/ChatControl.tsx +++ b/web/src/components/ChatControl.tsx @@ -21,6 +21,7 @@ interface ChatMessage { content: string; profileData?: any; action?: ChatAssistantAction; + insights?: string[]; nextActions?: string[]; quickLinks?: ChatQuickLink[]; timestamp: Date; @@ -36,6 +37,9 @@ type ChatAssistantAction = | 'create_profile' | 'update_profile' | 'recommend_profile_change' + | 'recommend_trade_plan' + | 'recommend_reconciliation_followup' + | 'review_recent_trades' | 'explain' | 'explain_position' | 'explain_waiting' @@ -61,6 +65,9 @@ export const BASE_QUICK_ACTIONS: QuickAction[] = [ { label: 'Why no trade?', prompt: 'Why has no trade fired yet for my active profile? Explain what the bot is waiting for.' }, { label: 'Explain blocker', prompt: 'Why is a trade or exit blocked right now? Explain the main blocker.' }, { label: 'Recon summary', prompt: 'Summarize reconciliation health, stale orders, and any manual review risk right now.' }, + { label: 'Fix reconciliation', prompt: 'What should I do about reconciliation right now? Recommend the safest follow-up.' }, + { label: 'Review recent trades', prompt: 'Review my recent trades and tell me what to focus on next.' }, + { label: 'Manage live holding', prompt: 'Recommend the safest Trade Plan action for my current live holding.' }, { label: 'What rules?', prompt: 'What rules should I use for a day trading strategy?' }, { label: 'Modify existing', prompt: 'Show me my existing profiles and suggest improvements' }, ]; @@ -114,6 +121,24 @@ export const normalizeProfileForApply = (profileData: any) => ({ const isProfileMutationAction = (action?: ChatAssistantAction): action is 'create_profile' | 'update_profile' | 'recommend_profile_change' => action === 'create_profile' || action === 'update_profile' || action === 'recommend_profile_change'; +const formatChatError = (error: unknown) => { + const message = String((error as { message?: string })?.message || '').trim(); + const lower = message.toLowerCase(); + if (!message) { + return 'The assistant could not complete that request. Please try again.'; + } + if (lower.includes('not authenticated') || lower.includes('unauthorized') || lower.includes('forbidden')) { + return 'Your session is not authorized for chat right now. Sign in again, then retry.'; + } + if (lower.includes('failed to fetch') || lower.includes('network') || lower.includes('load failed')) { + return 'The chat service is temporarily unreachable. Check connectivity and retry.'; + } + if (lower.includes('timeout')) { + return 'The chat request timed out before the assistant finished. Retry once the backend is responsive.'; + } + return `The assistant could not complete that request: ${message}`; +}; + const summarizeRuntimeContext = (botState: BotState) => ({ signalContexts: Object.entries(botState.symbols ?? {}) .flatMap(([symbol, symbolState]) => @@ -423,6 +448,7 @@ export const ChatControl = ({ profiles, botState, onApplyProfile }: ChatControlP content: data.summary || data.reasoning || 'Profile configuration generated.', profileData: data.profile || null, action: data.action as ChatAssistantAction | undefined, + insights: Array.isArray(data.insights) ? data.insights.map((entry: unknown) => String(entry)) : undefined, nextActions: Array.isArray(data.nextActions) ? data.nextActions.map((entry: unknown) => String(entry)) : undefined, quickLinks: Array.isArray(data.quickLinks) ? data.quickLinks as ChatQuickLink[] : undefined, timestamp: new Date(), @@ -437,7 +463,7 @@ export const ChatControl = ({ profiles, botState, onApplyProfile }: ChatControlP setMessages(prev => [...prev, { id: Date.now() + 1, role: 'assistant', - content: `Error: ${err.message}`, + content: formatChatError(err), timestamp: new Date(), }]); } @@ -704,6 +730,32 @@ export const ChatControl = ({ profiles, botState, onApplyProfile }: ChatControlP ) : null} + {msg.insights && msg.insights.length > 0 ? ( +