Add reflection for each analysis

This commit is contained in:
Jenit Jain 2025-08-23 22:01:48 -07:00
parent ce86231796
commit 4c7f636097
6 changed files with 388 additions and 69 deletions

View File

@ -49,7 +49,12 @@ class FinancialSituationMemory:
embeddings=embeddings,
ids=ids,
)
def get_latest_situation(self):
"""Retrieve the latest entry from a situation_collection."""
latest_entry = self.situation_collection.query(sort_by="id:descending", limit=1)
return latest_entry
def get_memories(self, current_situation, n_matches=1):
"""Find matching recommendations using OpenAI embeddings"""
query_embedding = self.get_embedding(current_situation)

View File

@ -4,6 +4,7 @@ from pydantic import BaseModel
from typing import Dict, Any, Optional
import json
import os
import time
from datetime import datetime
import glob
import uuid
@ -59,10 +60,12 @@ class AnalysisResponse(BaseModel):
class JobStatus(BaseModel):
job_id: str
start_time: Optional[float] = None
status: str
progress: Optional[str] = None
result: Optional[Dict[str, Any]] = None
error: Optional[str] = None
trading_agent: Any = None # Store the trading agent instance here
# In-memory job storage (in production, use Redis or database)
jobs: Dict[str, JobStatus] = {}
@ -77,6 +80,15 @@ async def health_check():
async def run_analysis_task(job_id: str, symbol: str, analysis_date: str, config_overrides: Dict[str, Any] = None):
"""Background task to run the trading analysis without blocking the event loop"""
def execution_time() -> Optional[str]:
if jobs[job_id].status == "completed" or jobs[job_id].status == "failed":
return f"{time.time() - jobs[job_id].start_time:.2f} seconds"
else:
return None
jobs[job_id].start_time = time.time() # Save the start time in the job
try:
jobs[job_id].status = "running"
jobs[job_id].progress = "Initializing TradingAgents..."
@ -87,16 +99,13 @@ async def run_analysis_task(job_id: str, symbol: str, analysis_date: str, config
config.update(config_overrides)
jobs[job_id].progress = "Setting up trading graph..."
# Define blocking work as sync function
def _do_work():
ta = TradingAgentsGraph(debug=True, config=config)
jobs[job_id].progress = f"Analyzing {symbol} for {analysis_date}..."
_, decision = ta.propagate(symbol, analysis_date)
return decision
# Run blocking work in threadpool so the event loop stays responsive
decision = await run_in_threadpool(_do_work)
# Create and store the trading agent instance
ta = TradingAgentsGraph(debug=True, config=config)
jobs[job_id].trading_agent = ta # Store the instance
jobs[job_id].progress = f"Analyzing {symbol} for {analysis_date}..."
# Run the propagate method in a threadpool
_, decision = await run_in_threadpool(ta.propagate, symbol, analysis_date)
jobs[job_id].status = "completed"
jobs[job_id].result = {
@ -104,6 +113,20 @@ async def run_analysis_task(job_id: str, symbol: str, analysis_date: str, config
"date": analysis_date,
"decision": decision,
"completed_at": datetime.now().isoformat(),
"execution_time": execution_time()
}
jobs[job_id].progress = "Analysis completed successfully"
except Exception as e:
jobs[job_id].status = "failed"
jobs[job_id].error = str(e)
jobs[job_id].progress = f"Error: {str(e)}"
jobs[job_id].result = {
"symbol": symbol,
"date": analysis_date,
"decision": decision,
"completed_at": datetime.now().isoformat(),
"execution_time": execution_time()
}
jobs[job_id].progress = "Analysis completed successfully"
@ -342,15 +365,81 @@ async def get_jobs():
"""Get all jobs"""
job_lst = []
for job_id, job in jobs.items():
job_lst.append({
job_dict = {
"job_id": job_id,
"status": job.status,
"progress": job.progress,
"result": job.result,
"error": job.error
})
"error": job.error,
}
# Add execution_time if available
if hasattr(job, 'start_time') and job.start_time is not None:
if job.status in ["completed", "failed"]:
job_dict["execution_time"] = f"{time.time() - job.start_time:.2f} seconds"
job_lst.append(job_dict)
return {"jobs": job_lst}
@app.post("/reflect-on-analysis/{symbol}/{date}")
async def reflect_on_analysis(symbol: str, date: str, request: dict):
"""Get latest financial situation memory for a specific analysis"""
returns_losses = request.get("returns_losses")
if returns_losses is None:
raise HTTPException(status_code=400, detail="returns_losses is required in request body")
# Find the job that matches the symbol and date
matching_job = None
for job_id, job in jobs.items():
if (job.result.get("symbol") == symbol.upper() and
job.result.get("date") == date and
hasattr(job, 'trading_agent') and
job.trading_agent):
matching_job = job
break
if not matching_job:
raise HTTPException(status_code=404, detail=f"No active job found for {symbol} on {date}")
if not hasattr(matching_job.trading_agent, 'memory') or not matching_job.trading_agent.memory:
raise HTTPException(status_code=404, detail="No memory found for this analysis")
matching_job.trading_agent.reflect_and_remember(returns_losses)
try:
bull_memory = matching_job.trading_agent.bull_memory
bear_memory = matching_job.trading_agent.bear_memory
trader_memory = matching_job.trading_agent.trader_memory
invest_judge_memory = matching_job.trading_agent.invest_judge_memory
risk_manager_memory = matching_job.trading_agent.risk_manager_memory
reflections = {}
latest_entry = bull_memory.get_latest_situation()
reflections["bull_memory"] = latest_entry
latest_entry = bear_memory.get_latest_situation()
reflections["bear_memory"] = latest_entry
latest_entry = trader_memory.get_latest_situation()
reflections["trader_memory"] = latest_entry
latest_entry = invest_judge_memory.get_latest_situation()
reflections["invest_judge_memory"] = latest_entry
latest_entry = risk_manager_memory.get_latest_situation()
reflections["risk_manager_memory"] = latest_entry
return {
"symbol": symbol.upper(),
"date": date,
"job_id": matching_job.job_id,
"reflections": reflections
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error retrieving latest situation: {str(e)}")
@app.get("/config")
async def get_default_config():
"""Get the default configuration"""
@ -358,4 +447,4 @@ async def get_default_config():
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)

View File

@ -687,7 +687,13 @@ function App() {
{selectedTransformedData ? (
<TransformedDataAdapter analysisData={selectedTransformedData} />
) : resultDetail ? (
<AnalysisDataAdapter tradingResult={resultDetail} />
<AnalysisDataAdapter
tradingResult={{
...resultDetail,
symbol: selectedResult?.company || resultDetail?.symbol,
date: selectedResult?.date || resultDetail?.date
}}
/>
) : (
<div className="p-8 text-center">
<div className="text-gray-500 mb-2">Loading analysis data...</div>

View File

@ -3,6 +3,7 @@ import AnalysisWidgets from '../pages/AnalysisWidgets.tsx';
interface TradingAgentsResult {
symbol: string;
date?: string;
final_decision?: {
decision: string;
reasoning: string;
@ -90,7 +91,7 @@ const AnalysisDataAdapter: React.FC<AnalysisDataAdapterProps> = ({ tradingResult
'Based on comprehensive analysis, the stock shows strong fundamentals with reasonable valuation and positive technical momentum, warranting a buy recommendation with appropriate risk management.'
};
return <AnalysisWidgets data={transformedData} rawData={tradingResult} />;
return <AnalysisWidgets symbol={tradingResult.symbol} date={tradingResult.date} data={transformedData} rawData={tradingResult} />;
};
export default AnalysisDataAdapter;

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, { useState } from 'react';
// New interface for the transformed JSON structure
interface TransformedAnalysisData {
@ -458,12 +458,77 @@ const TransformedDataAdapter: React.FC<TransformedDataAdapterProps> = ({ analysi
const [activeReportTab, setActiveReportTab] = React.useState<string>(availableReports[0]?.key || 'market');
React.useEffect(() => { setActiveReportTab(availableReports[0]?.key || 'market'); }, [availableReports]);
// Reflection functionality state
const [showReflectionModal, setShowReflectionModal] = React.useState(false);
const [lossResults, setLossResults] = React.useState('');
const [isReflecting, setIsReflecting] = React.useState(false);
const [reflectionResults, setReflectionResults] = React.useState(null);
const [reflectionError, setReflectionError] = React.useState('');
// Handle reflection API call
const handleReflection = async () => {
const symbolToUse = data.metadata.company_ticker;
const dateToUse = data.metadata.analysis_date;
console.log('Reflection attempt:', { symbolToUse, dateToUse, lossResults });
if (!lossResults.trim()) {
setReflectionError('Please enter your loss results');
return;
}
setIsReflecting(true);
setReflectionError('');
try {
const response = await fetch(`http://localhost:8000/reflect-on-analysis/${symbolToUse}/${dateToUse}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
returns_losses: lossResults
})
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.detail || 'Failed to perform reflection');
}
const reflectionData = await response.json();
setReflectionResults(reflectionData);
setReflectionError('');
} catch (error) {
console.error('Reflection error:', error);
setReflectionError(error.message || 'Failed to perform reflection');
} finally {
setIsReflecting(false);
}
};
const handleModalClose = () => {
setShowReflectionModal(false);
setLossResults('');
setReflectionResults(null);
setReflectionError('');
};
return (
<div className="p-6">
<div className="mx-auto max-w-6xl space-y-4">
{/* Metadata - Full width */}
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-lg font-semibold mb-3">Company Information</h2>
<div className="flex justify-between items-center mb-3">
<h2 className="text-lg font-semibold">Company Information</h2>
<button
onClick={() => setShowReflectionModal(true)}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 text-sm font-medium"
title="Reflect on Analysis"
>
🤔 Reflect on Analysis
</button>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<Stat label="Company" value={`${md?.company_name ?? '-'} (${md?.company_ticker ?? '-'})`} />
<Stat label="Analysis Date" value={md?.analysis_date ?? '-'} />
@ -595,12 +660,85 @@ const TransformedDataAdapter: React.FC<TransformedDataAdapterProps> = ({ analysi
</div>
</div>
</div>
{/* Reflection Modal */}
{showReflectionModal && (
<div className="fixed inset-0 bg-gray-900 bg-opacity-75 flex justify-center items-center z-50">
<div className="bg-white rounded-lg shadow-lg p-6 w-full max-w-md mx-4">
<div className="flex justify-between items-center mb-4">
<h2 className="text-lg font-semibold">Reflect on Analysis</h2>
<button
onClick={handleModalClose}
className="text-gray-400 hover:text-gray-600"
>
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<form onSubmit={(e) => { e.preventDefault(); handleReflection(); }}>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-2">
Loss Results:
</label>
<textarea
value={lossResults}
onChange={(e) => setLossResults(e.target.value)}
placeholder="e.g., Lost 15% due to unexpected earnings miss, market volatility, etc."
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
rows={4}
/>
{reflectionError && (
<p className="mt-2 text-sm text-red-500">{reflectionError}</p>
)}
</div>
<div className="flex justify-end space-x-3">
<button
type="button"
onClick={handleModalClose}
className="px-4 py-2 bg-gray-300 text-gray-700 rounded hover:bg-gray-400"
>
Cancel
</button>
<button
type="submit"
disabled={isReflecting}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"
>
{isReflecting ? 'Reflecting...' : 'Submit Reflection'}
</button>
</div>
</form>
{/* Display reflection results */}
{reflectionResults && (
<div className="mt-4 p-4 bg-gray-50 rounded-md">
<h4 className="text-sm font-medium text-gray-900 mb-2">Reflection Results:</h4>
<div className="space-y-2">
{Object.entries(reflectionResults.reflections || {}).map(([agentType, reflection]) => (
<div key={agentType} className="text-xs">
<span className="font-medium capitalize">{agentType.replace('_', ' ')}:</span>
<p className="text-gray-600 ml-2">{typeof reflection === 'string' ? reflection : JSON.stringify(reflection)}</p>
</div>
))}
</div>
</div>
)}
</div>
</div>
)}
</div>
);
};
// Render minimalist dashboard with all keys from the transformed JSON
return <MinimalTransformedDashboard data={transformedWithConfig} />;
return (
<div>
<MinimalTransformedDashboard data={transformedWithConfig} />
</div>
);
};
export default TransformedDataAdapter;

View File

@ -34,14 +34,24 @@ interface AnalysisWidgetsProps {
rawData?: any;
onBackWidget?: () => void;
onRefreshWidget?: () => void;
symbol?: string;
date?: string;
}
const AnalysisWidgets: React.FC<AnalysisWidgetsProps> = ({ data, rawData, onBackWidget, onRefreshWidget }) => {
const AnalysisWidgets: React.FC<AnalysisWidgetsProps> = ({ data, rawData, onBackWidget, onRefreshWidget, symbol, date }) => {
const [activeTab, setActiveTab] = useState('bull');
const [expandedSections, setExpandedSections] = useState<Set<string>>(new Set());
const [positionSize, setPositionSize] = useState(10000);
const [portfolioAllocation, setPortfolioAllocation] = useState(4);
const [showReflectionModal, setShowReflectionModal] = useState(false);
const [lossResults, setLossResults] = useState('');
const [isReflecting, setIsReflecting] = useState(false);
const [reflectionResults, setReflectionResults] = useState(null);
const [reflectionError, setReflectionError] = useState('');
console.log('AnalysisWidgets props:', { symbol, date, hasRawData: !!rawData });
const handleBack = () => {
if (onBackWidget) return onBackWidget();
if (typeof window !== 'undefined' && window.history) window.history.back();
@ -51,6 +61,47 @@ const AnalysisWidgets: React.FC<AnalysisWidgetsProps> = ({ data, rawData, onBack
if (typeof window !== 'undefined') window.location.reload();
};
const handleReflection = async () => {
const symbolToUse = symbol || data.symbol || 'UNKNOWN';
const dateToUse = date || new Date().toISOString().split('T')[0];
console.log('Reflection attempt:', { symbolToUse, dateToUse, lossResults });
if (!lossResults.trim()) {
setReflectionError('Please enter your loss results');
return;
}
setIsReflecting(true);
setReflectionError('');
try {
const response = await fetch(`http://localhost:8000/reflect-on-analysis/${symbolToUse}/${dateToUse}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
returns_losses: lossResults
})
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.detail || 'Failed to perform reflection');
}
const reflectionData = await response.json();
setReflectionResults(reflectionData);
setReflectionError('');
} catch (error) {
console.error('Reflection error:', error);
setReflectionError(error.message || 'Failed to perform reflection');
} finally {
setIsReflecting(false);
}
};
const WidgetHeader: React.FC<{ title: string }> = ({ title }) => (
<div className="flex items-center justify-between mb-4">
<button
@ -63,57 +114,33 @@ const AnalysisWidgets: React.FC<AnalysisWidgetsProps> = ({ data, rawData, onBack
</button>
<h3 className="text-lg font-semibold text-gray-900">{title}</h3>
<button
type="button"
onClick={handleRefresh}
className="p-2 rounded hover:bg-gray-100 text-gray-600"
aria-label="Refresh"
title="Refresh"
>
</button>
<div className="flex items-center space-x-2">
<button
type="button"
onClick={() => setShowReflectionModal(true)}
className="px-3 py-1 bg-blue-600 text-white rounded hover:bg-blue-700 text-sm"
title={`Reflect on Analysis (Symbol: ${symbol || data.symbol || 'N/A'}, Date: ${date || 'N/A'})`}
>
🤔 Reflect
</button>
<button
type="button"
onClick={handleRefresh}
className="p-2 rounded hover:bg-gray-100 text-gray-600"
aria-label="Refresh"
title="Refresh"
>
</button>
</div>
</div>
);
// Mock price data for chart
const priceData = [
{ date: '2024-07-01', price: 4.20, volume: 1200000 },
{ date: '2024-07-08', price: 4.45, volume: 1500000 },
{ date: '2024-07-15', price: 4.80, volume: 1800000 },
{ date: '2024-07-22', price: 5.20, volume: 2100000 },
{ date: '2024-07-26', price: 5.81, volume: 2500000 },
];
const ownershipData = [
{ name: 'Insider', value: data.insiderOwnership, color: '#8884d8' },
{ name: 'Institutional', value: data.institutionalOwnership, color: '#82ca9d' },
{ name: 'Retail', value: data.retailOwnership, color: '#ffc658' },
];
const toggleSection = (section: string) => {
const newExpanded = new Set(expandedSections);
if (newExpanded.has(section)) {
newExpanded.delete(section);
} else {
newExpanded.add(section);
}
setExpandedSections(newExpanded);
};
const calculatePosition = () => {
const allocation = (portfolioAllocation / 100) * positionSize;
const shares = Math.floor(allocation / data.currentPrice);
return { allocation, shares };
};
const getRiskColor = (level: number) => {
if (level < 30) return 'text-green-600';
if (level < 70) return 'text-yellow-600';
return 'text-red-600';
};
const getTrendColor = (current: number, reference: number) => {
return current > reference ? 'text-green-600' : 'text-red-600';
const handleModalClose = () => {
setShowReflectionModal(false);
setLossResults('');
setReflectionResults(null);
setReflectionError('');
};
return (
@ -554,6 +581,59 @@ const AnalysisWidgets: React.FC<AnalysisWidgetsProps> = ({ data, rawData, onBack
</div>
)}
</div>
{/* Reflection Modal */}
{showReflectionModal && (
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity">
<div className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div className="sm:flex sm:items-start">
<div className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
<svg className="h-6 w-6 text-red-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 className="text-lg leading-6 font-medium text-gray-900" id="modal-headline">
Reflect on Analysis
</h3>
<div className="mt-2">
<p className="text-sm text-gray-500">
Please enter your loss results to reflect on the analysis.
</p>
<input
type="text"
value={lossResults}
onChange={(e) => setLossResults(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
{reflectionError && (
<p className="text-sm text-red-500">{reflectionError}</p>
)}
</div>
</div>
</div>
</div>
<div className="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<button
type="button"
onClick={handleReflection}
disabled={isReflecting}
className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm"
>
{isReflecting ? 'Reflecting...' : 'Reflect'}
</button>
<button
type="button"
onClick={handleModalClose}
className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
>
Cancel
</button>
</div>
</div>
</div>
)}
</div>
);
};