Abid Ali Awan
commited on
Commit
Β·
5ad2796
1
Parent(s):
6ceb32c
Update README.md to enhance project description, add detailed features, installation instructions, example prompts, and technical architecture for the Financial Advisory Agent. Changed emoji and color scheme for better representation.
Browse files- .gitignore +194 -0
- README.md +212 -3
- agents/__init__.py +0 -0
- agents/financial_agent.py +263 -0
- agents/tools.py +696 -0
- app.py +747 -0
- public/images/fin_logo.png +0 -0
- requirements.txt +10 -0
.gitignore
ADDED
@@ -0,0 +1,194 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Byte-compiled / optimized / DLL files
|
2 |
+
__pycache__/
|
3 |
+
*.py[cod]
|
4 |
+
*$py.class
|
5 |
+
|
6 |
+
# C extensions
|
7 |
+
*.so
|
8 |
+
|
9 |
+
# Distribution / packaging
|
10 |
+
.Python
|
11 |
+
build/
|
12 |
+
develop-eggs/
|
13 |
+
dist/
|
14 |
+
downloads/
|
15 |
+
eggs/
|
16 |
+
.eggs/
|
17 |
+
lib/
|
18 |
+
lib64/
|
19 |
+
parts/
|
20 |
+
sdist/
|
21 |
+
var/
|
22 |
+
wheels/
|
23 |
+
share/python-wheels/
|
24 |
+
*.egg-info/
|
25 |
+
.installed.cfg
|
26 |
+
*.egg
|
27 |
+
MANIFEST
|
28 |
+
|
29 |
+
# PyInstaller
|
30 |
+
# Usually these files are written by a python script from a template
|
31 |
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
32 |
+
*.manifest
|
33 |
+
*.spec
|
34 |
+
|
35 |
+
# Installer logs
|
36 |
+
pip-log.txt
|
37 |
+
pip-delete-this-directory.txt
|
38 |
+
|
39 |
+
# Unit test / coverage reports
|
40 |
+
htmlcov/
|
41 |
+
.tox/
|
42 |
+
.nox/
|
43 |
+
.coverage
|
44 |
+
.coverage.*
|
45 |
+
.cache
|
46 |
+
nosetests.xml
|
47 |
+
coverage.xml
|
48 |
+
*.cover
|
49 |
+
*.py,cover
|
50 |
+
.hypothesis/
|
51 |
+
.pytest_cache/
|
52 |
+
cover/
|
53 |
+
|
54 |
+
# Translations
|
55 |
+
*.mo
|
56 |
+
*.pot
|
57 |
+
|
58 |
+
# Django stuff:
|
59 |
+
*.log
|
60 |
+
local_settings.py
|
61 |
+
db.sqlite3
|
62 |
+
db.sqlite3-journal
|
63 |
+
|
64 |
+
# Flask stuff:
|
65 |
+
instance/
|
66 |
+
.webassets-cache
|
67 |
+
|
68 |
+
# Scrapy stuff:
|
69 |
+
.scrapy
|
70 |
+
|
71 |
+
# Sphinx documentation
|
72 |
+
docs/_build/
|
73 |
+
|
74 |
+
# PyBuilder
|
75 |
+
.pybuilder/
|
76 |
+
target/
|
77 |
+
|
78 |
+
# Jupyter Notebook
|
79 |
+
.ipynb_checkpoints
|
80 |
+
|
81 |
+
# IPython
|
82 |
+
profile_default/
|
83 |
+
ipython_config.py
|
84 |
+
|
85 |
+
# pyenv
|
86 |
+
# For a library or package, you might want to ignore these files since the code is
|
87 |
+
# intended to run in multiple environments; otherwise, check them in:
|
88 |
+
# .python-version
|
89 |
+
|
90 |
+
# pipenv
|
91 |
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
92 |
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
93 |
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
94 |
+
# install all needed dependencies.
|
95 |
+
#Pipfile.lock
|
96 |
+
|
97 |
+
# UV
|
98 |
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
99 |
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
100 |
+
# commonly ignored for libraries.
|
101 |
+
#uv.lock
|
102 |
+
|
103 |
+
# poetry
|
104 |
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
105 |
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
106 |
+
# commonly ignored for libraries.
|
107 |
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
108 |
+
#poetry.lock
|
109 |
+
|
110 |
+
# pdm
|
111 |
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
112 |
+
#pdm.lock
|
113 |
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
114 |
+
# in version control.
|
115 |
+
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
116 |
+
.pdm.toml
|
117 |
+
.pdm-python
|
118 |
+
.pdm-build/
|
119 |
+
|
120 |
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
121 |
+
__pypackages__/
|
122 |
+
|
123 |
+
# Celery stuff
|
124 |
+
celerybeat-schedule
|
125 |
+
celerybeat.pid
|
126 |
+
|
127 |
+
# SageMath parsed files
|
128 |
+
*.sage.py
|
129 |
+
|
130 |
+
# Environments
|
131 |
+
.env
|
132 |
+
.venv
|
133 |
+
env/
|
134 |
+
venv/
|
135 |
+
ENV/
|
136 |
+
env.bak/
|
137 |
+
venv.bak/
|
138 |
+
|
139 |
+
# Spyder project settings
|
140 |
+
.spyderproject
|
141 |
+
.spyproject
|
142 |
+
|
143 |
+
# Rope project settings
|
144 |
+
.ropeproject
|
145 |
+
|
146 |
+
# mkdocs documentation
|
147 |
+
/site
|
148 |
+
|
149 |
+
# mypy
|
150 |
+
.mypy_cache/
|
151 |
+
.dmypy.json
|
152 |
+
dmypy.json
|
153 |
+
|
154 |
+
# Pyre type checker
|
155 |
+
.pyre/
|
156 |
+
|
157 |
+
# pytype static type analyzer
|
158 |
+
.pytype/
|
159 |
+
|
160 |
+
# Cython debug symbols
|
161 |
+
cython_debug/
|
162 |
+
|
163 |
+
# PyCharm
|
164 |
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
165 |
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
166 |
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
167 |
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
168 |
+
#.idea/
|
169 |
+
|
170 |
+
# Abstra
|
171 |
+
# Abstra is an AI-powered process automation framework.
|
172 |
+
# Ignore directories containing user credentials, local state, and settings.
|
173 |
+
# Learn more at https://abstra.io/docs
|
174 |
+
.abstra/
|
175 |
+
|
176 |
+
# Visual Studio Code
|
177 |
+
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
178 |
+
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
179 |
+
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
180 |
+
# you could uncomment the following to ignore the enitre vscode folder
|
181 |
+
# .vscode/
|
182 |
+
|
183 |
+
# Ruff stuff:
|
184 |
+
.ruff_cache/
|
185 |
+
|
186 |
+
# PyPI configuration file
|
187 |
+
.pypirc
|
188 |
+
|
189 |
+
# Cursor
|
190 |
+
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
|
191 |
+
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
|
192 |
+
# refer to https://docs.cursor.com/context/ignore-files
|
193 |
+
.cursorignore
|
194 |
+
.cursorindexingignore
|
README.md
CHANGED
@@ -1,8 +1,8 @@
|
|
1 |
---
|
2 |
title: Financial Advisory Agent
|
3 |
-
emoji:
|
4 |
colorFrom: gray
|
5 |
-
colorTo:
|
6 |
sdk: gradio
|
7 |
sdk_version: 5.33.0
|
8 |
app_file: app.py
|
@@ -11,4 +11,213 @@ license: apache-2.0
|
|
11 |
short_description: Financial analysis, investments, budget planning, and more.
|
12 |
---
|
13 |
|
14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
---
|
2 |
title: Financial Advisory Agent
|
3 |
+
emoji: π§βπΌ
|
4 |
colorFrom: gray
|
5 |
+
colorTo: blue
|
6 |
sdk: gradio
|
7 |
sdk_version: 5.33.0
|
8 |
app_file: app.py
|
|
|
11 |
short_description: Financial analysis, investments, budget planning, and more.
|
12 |
---
|
13 |
|
14 |
+
|
15 |
+
# AI Financial Advisory Agent
|
16 |
+
|
17 |
+
An intelligent financial advisory agent powered by OpenAI's GPT-4.1 and specialized financial tools. This agent provides comprehensive financial analysis, investment recommendations, budget planning, and market insights through an intuitive web interface.
|
18 |
+
|
19 |
+
## π Features
|
20 |
+
|
21 |
+
### Core Financial Tools
|
22 |
+
- **π Investment Analyzer**: Real-time stock analysis with technical indicators, risk assessment, and investment recommendations
|
23 |
+
- **π° Budget Planner**: Personalized budget creation using the 50/30/20 rule with optimization suggestions
|
24 |
+
- **π³ Expense Tracker**: Advanced expense analysis with trends, predictions, and anomaly detection
|
25 |
+
- **π Portfolio Analyzer**: Portfolio diversification analysis and performance optimization
|
26 |
+
- **π° Market Trends Analyzer**: Real-time market news, sector analysis, and economic insights
|
27 |
+
|
28 |
+
### Advanced Features
|
29 |
+
- **π― Smart Response Type Detection**: Automatically detects if users want short summaries or detailed reports
|
30 |
+
- **β‘ Real-time LLM Streaming**: Live token-by-token response generation from OpenAI API
|
31 |
+
- **π Interactive Tool Results**: Collapsible sections for clean data presentation
|
32 |
+
- **π Comprehensive Data Analysis**: Enhanced visualizations and insights for all financial data
|
33 |
+
- **β±οΈ Real-time Status Updates**: Shows which APIs are being called and estimated completion times
|
34 |
+
|
35 |
+
## π οΈ Installation
|
36 |
+
|
37 |
+
1. **Clone the repository**
|
38 |
+
```bash
|
39 |
+
git clone https://github.com/yourusername/financial-advisory-agent.git
|
40 |
+
cd financial-advisory-agent
|
41 |
+
```
|
42 |
+
|
43 |
+
2. **Install dependencies**
|
44 |
+
```bash
|
45 |
+
pip install -r requirements.txt
|
46 |
+
```
|
47 |
+
|
48 |
+
3. **Set up environment variables**
|
49 |
+
Create a `.env` file or set environment variables:
|
50 |
+
```bash
|
51 |
+
export OPENAI_API_KEY="your-openai-api-key"
|
52 |
+
export TAVILY_API_KEY="your-tavily-api-key"
|
53 |
+
```
|
54 |
+
|
55 |
+
4. **Run the application**
|
56 |
+
```bash
|
57 |
+
python app.py
|
58 |
+
```
|
59 |
+
|
60 |
+
## π¬ Example Prompts
|
61 |
+
|
62 |
+
### Single Tool Invocation
|
63 |
+
|
64 |
+
#### Investment Analyzer
|
65 |
+
```
|
66 |
+
"Analyze AAPL stock and tell me if it's a good investment"
|
67 |
+
"Should I buy Tesla stock right now?"
|
68 |
+
"Give me a detailed analysis of Microsoft shares"
|
69 |
+
"Quick summary of NVDA stock performance"
|
70 |
+
```
|
71 |
+
|
72 |
+
#### Budget Planner
|
73 |
+
```
|
74 |
+
"Help me create a budget with $5000 monthly income and expenses: rent $1500, food $500, utilities $200"
|
75 |
+
"I make $75,000 annually, help me budget my expenses"
|
76 |
+
"Create a budget plan for someone earning $4000/month"
|
77 |
+
"Quick budget advice for $6000 monthly income"
|
78 |
+
```
|
79 |
+
|
80 |
+
#### Market Trends Analyzer
|
81 |
+
```
|
82 |
+
"What are the latest market trends in tech stocks?"
|
83 |
+
"Give me current market news and analysis"
|
84 |
+
"Detailed report on today's market movements"
|
85 |
+
"Quick market update for technology sector"
|
86 |
+
```
|
87 |
+
|
88 |
+
#### Portfolio Analyzer
|
89 |
+
```
|
90 |
+
"Analyze my portfolio: {'holdings': [{'symbol': 'AAPL', 'shares': 100}, {'symbol': 'GOOGL', 'shares': 50}]}"
|
91 |
+
"Check my diversification with holdings: AAPL 40%, MSFT 30%, TSLA 30%"
|
92 |
+
"Detailed portfolio analysis for my current investments"
|
93 |
+
```
|
94 |
+
|
95 |
+
#### Expense Tracker
|
96 |
+
```
|
97 |
+
"Track my expenses: [{'category': 'food', 'amount': 150}, {'category': 'gas', 'amount': 80}]"
|
98 |
+
"Analyze my spending patterns from last month"
|
99 |
+
"Help me categorize and analyze my expenses"
|
100 |
+
```
|
101 |
+
|
102 |
+
### Response Type Control
|
103 |
+
|
104 |
+
#### Short Responses (Under 200 words)
|
105 |
+
```
|
106 |
+
"Quick analysis of AAPL"
|
107 |
+
"Brief summary of market trends"
|
108 |
+
"Give me a short budget recommendation"
|
109 |
+
"Simple portfolio check"
|
110 |
+
"Fast expense analysis"
|
111 |
+
```
|
112 |
+
|
113 |
+
#### Detailed Responses (Comprehensive reports)
|
114 |
+
```
|
115 |
+
"Give me a detailed report on AAPL stock"
|
116 |
+
"Comprehensive market analysis for tech sector"
|
117 |
+
"Thorough budget breakdown with recommendations"
|
118 |
+
"In-depth portfolio diversification analysis"
|
119 |
+
"Complete expense tracking report with insights"
|
120 |
+
```
|
121 |
+
|
122 |
+
### Multi-Tool Queries
|
123 |
+
```
|
124 |
+
"Analyze AAPL stock and create a budget for investing $1000 monthly"
|
125 |
+
"What are the current market trends and how should I adjust my portfolio?"
|
126 |
+
"Help me budget $5000 income and recommend some good stocks to invest in"
|
127 |
+
"Track my expenses and suggest a better budget allocation"
|
128 |
+
```
|
129 |
+
|
130 |
+
### Advanced Queries
|
131 |
+
```
|
132 |
+
"I have $50K to invest. Analyze current market trends, suggest a diversified portfolio, and create a budget for monthly investments of $2000"
|
133 |
+
"Detailed analysis: Review AAPL, MSFT, and GOOGL stocks, then help me rebalance my portfolio"
|
134 |
+
"Comprehensive financial review: Budget analysis for $8000 monthly income, expense tracking, and investment recommendations"
|
135 |
+
```
|
136 |
+
|
137 |
+
## π― Response Types
|
138 |
+
|
139 |
+
The system automatically detects your preference:
|
140 |
+
|
141 |
+
### Short Response Keywords
|
142 |
+
- `quick`, `brief`, `short`, `summary`, `concise`, `simple`, `fast`, `tldr`, `bottom line`
|
143 |
+
|
144 |
+
### Detailed Response Keywords
|
145 |
+
- `detailed`, `comprehensive`, `thorough`, `in-depth`, `report`, `breakdown`, `explain`, `elaborate`, `deep dive`
|
146 |
+
|
147 |
+
### Default Behavior
|
148 |
+
- **Short responses** are the default (under 200 words)
|
149 |
+
- **Detailed responses** are triggered by specific keywords (comprehensive analysis)
|
150 |
+
|
151 |
+
## π§ Technical Architecture
|
152 |
+
|
153 |
+
- **Frontend**: Gradio web interface with real-time streaming
|
154 |
+
- **Backend**: LangChain agents with OpenAI GPT-4
|
155 |
+
- **Data Sources**:
|
156 |
+
- Yahoo Finance API for stock data
|
157 |
+
- Tavily Search API for market news
|
158 |
+
- Real-time financial calculations
|
159 |
+
- **Streaming**: Direct LLM token streaming from OpenAI API
|
160 |
+
- **Tools**: Specialized financial analysis functions
|
161 |
+
|
162 |
+
## π Tool Capabilities
|
163 |
+
|
164 |
+
### Investment Analyzer
|
165 |
+
- Real-time stock prices and historical data
|
166 |
+
- Technical indicators (RSI, MACD, Bollinger Bands, Moving Averages)
|
167 |
+
- Risk assessment (volatility, VaR, beta analysis)
|
168 |
+
- Fundamental analysis (P/E, P/B ratios, dividend yield)
|
169 |
+
- Buy/Hold/Sell recommendations with confidence scores
|
170 |
+
|
171 |
+
### Budget Planner
|
172 |
+
- 50/30/20 rule implementation
|
173 |
+
- Emergency fund calculations
|
174 |
+
- Debt-to-income ratio analysis
|
175 |
+
- Savings optimization recommendations
|
176 |
+
- Expense category warnings
|
177 |
+
|
178 |
+
### Market Trends Analyzer
|
179 |
+
- Real-time market news via Tavily Search
|
180 |
+
- Major index tracking (S&P 500, NASDAQ)
|
181 |
+
- Market sentiment analysis
|
182 |
+
- Key theme extraction
|
183 |
+
- Economic trend identification
|
184 |
+
|
185 |
+
### Portfolio Analyzer
|
186 |
+
- Diversification scoring
|
187 |
+
- Sector allocation analysis
|
188 |
+
- Concentration risk assessment
|
189 |
+
- Rebalancing recommendations
|
190 |
+
- Performance metrics
|
191 |
+
|
192 |
+
### Expense Tracker
|
193 |
+
- Category-wise spending analysis
|
194 |
+
- Trend detection and predictions
|
195 |
+
- Budget vs. actual comparisons
|
196 |
+
- Anomaly detection
|
197 |
+
- Spending pattern insights
|
198 |
+
|
199 |
+
## π Recent Updates
|
200 |
+
|
201 |
+
- β
**Smart Response Detection**: Automatically detects if users want short or detailed responses
|
202 |
+
- β
**Real LLM Streaming**: Genuine token-by-token streaming from OpenAI API
|
203 |
+
- β
**Enhanced Tool Status**: Real-time API call tracking with time estimates
|
204 |
+
- β
**Improved Data Analysis**: Comprehensive visualizations and insights
|
205 |
+
- β
**Optimized Performance**: Faster tool execution and response times
|
206 |
+
- β
**Collapsible Results**: Clean, organized tool output presentation
|
207 |
+
|
208 |
+
## π API Keys Required
|
209 |
+
|
210 |
+
- **OpenAI API Key**: For language model
|
211 |
+
- **Tavily API Key**: For real-time market news and trends
|
212 |
+
|
213 |
+
## Issues to Resolve
|
214 |
+
The problem is in the current flow:
|
215 |
+
1. Agent executor runs all tools
|
216 |
+
2. We collect ALL results
|
217 |
+
3. Then display everything at once
|
218 |
+
4. Then stream the final response
|
219 |
+
|
220 |
+
## π€ Contributing
|
221 |
+
|
222 |
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
223 |
+
|
agents/__init__.py
ADDED
File without changes
|
agents/financial_agent.py
ADDED
@@ -0,0 +1,263 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import json
|
2 |
+
import operator
|
3 |
+
import re
|
4 |
+
from typing import Annotated, List, Tuple, TypedDict, Union
|
5 |
+
|
6 |
+
from langchain.agents import AgentExecutor, create_openai_tools_agent
|
7 |
+
from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder
|
8 |
+
from langchain.schema import AIMessage, HumanMessage, SystemMessage
|
9 |
+
from langchain.tools import Tool
|
10 |
+
from langchain_openai import ChatOpenAI
|
11 |
+
|
12 |
+
|
13 |
+
class AgentState(TypedDict):
|
14 |
+
messages: Annotated[List[Union[HumanMessage, AIMessage]], operator.add]
|
15 |
+
context: dict
|
16 |
+
|
17 |
+
|
18 |
+
class FinancialAdvisorAgent:
|
19 |
+
def __init__(self, tools: List[Tool], openai_api_key: str):
|
20 |
+
self.tools = tools
|
21 |
+
self.llm = ChatOpenAI(
|
22 |
+
api_key=openai_api_key, model="gpt-4.1-mini-2025-04-14", temperature=0.7
|
23 |
+
)
|
24 |
+
self.tools_by_name = {tool.name: tool for tool in tools}
|
25 |
+
|
26 |
+
# Create agent with tools
|
27 |
+
self.system_prompt = """You are a professional financial advisor AI assistant with access to specialized tools.
|
28 |
+
|
29 |
+
Available tools:
|
30 |
+
- budget_planner: Use when users ask about budgeting, income allocation, or expense planning. Input should be JSON with 'income' and 'expenses' keys.
|
31 |
+
- investment_analyzer: Use when users ask about specific stocks or investments. Input should be a stock symbol (e.g., AAPL).
|
32 |
+
- expense_tracker: Use when users want to track or analyze expenses. Input should be JSON with 'expenses' array.
|
33 |
+
- market_trends: Use when users ask about market trends or financial news. Input should be a search query.
|
34 |
+
- portfolio_analyzer: Use when users want to analyze their portfolio. Input should be JSON with 'holdings' array.
|
35 |
+
|
36 |
+
IMPORTANT: You MUST use these tools when answering financial questions. Do not provide generic advice without using the appropriate tool first.
|
37 |
+
|
38 |
+
When a user asks a question:
|
39 |
+
1. Identify which tool is most appropriate
|
40 |
+
2. Extract or request the necessary information
|
41 |
+
3. Use the tool to get specific data
|
42 |
+
4. Provide advice based on the tool's output"""
|
43 |
+
|
44 |
+
self.prompt = ChatPromptTemplate.from_messages(
|
45 |
+
[
|
46 |
+
("system", self.system_prompt),
|
47 |
+
MessagesPlaceholder(variable_name="messages"),
|
48 |
+
("human", "{input}"),
|
49 |
+
MessagesPlaceholder(variable_name="agent_scratchpad"),
|
50 |
+
]
|
51 |
+
)
|
52 |
+
|
53 |
+
self.agent = create_openai_tools_agent(self.llm, self.tools, self.prompt)
|
54 |
+
self.agent_executor = AgentExecutor(
|
55 |
+
agent=self.agent,
|
56 |
+
tools=self.tools,
|
57 |
+
verbose=True,
|
58 |
+
return_intermediate_steps=True,
|
59 |
+
)
|
60 |
+
|
61 |
+
def _extract_tool_usage(self, intermediate_steps):
|
62 |
+
"""Extract tool usage from intermediate steps"""
|
63 |
+
tools_used = []
|
64 |
+
tool_results = []
|
65 |
+
|
66 |
+
for action, result in intermediate_steps:
|
67 |
+
if hasattr(action, "tool"):
|
68 |
+
tools_used.append(action.tool)
|
69 |
+
tool_results.append(result)
|
70 |
+
|
71 |
+
# Return the last tool used and its result for backward compatibility
|
72 |
+
# But also return all tools and results for multi-tool scenarios
|
73 |
+
if tools_used:
|
74 |
+
return tools_used[-1], tool_results[-1], tools_used, tool_results
|
75 |
+
return None, None, [], []
|
76 |
+
|
77 |
+
def _prepare_tool_input(self, message: str, tool_name: str) -> str:
|
78 |
+
"""Prepare input for specific tools based on the message"""
|
79 |
+
if tool_name == "investment_analyzer":
|
80 |
+
# Extract stock symbols
|
81 |
+
symbols = re.findall(r"\b[A-Z]{2,5}\b", message)
|
82 |
+
if symbols:
|
83 |
+
return symbols[0]
|
84 |
+
return "AAPL" # Default
|
85 |
+
|
86 |
+
elif tool_name == "budget_planner":
|
87 |
+
# Try to extract income and expenses from message
|
88 |
+
income_match = re.search(
|
89 |
+
r"\$?(\d+(?:,\d{3})*(?:\.\d{2})?)\s*(?:monthly\s*)?income",
|
90 |
+
message,
|
91 |
+
re.I,
|
92 |
+
)
|
93 |
+
income = (
|
94 |
+
float(income_match.group(1).replace(",", "")) if income_match else 5000
|
95 |
+
)
|
96 |
+
|
97 |
+
# Extract expenses
|
98 |
+
expenses = {}
|
99 |
+
expense_patterns = [
|
100 |
+
(r"rent:?\s*\$?(\d+(?:,\d{3})*(?:\.\d{2})?)", "rent"),
|
101 |
+
(r"food:?\s*\$?(\d+(?:,\d{3})*(?:\.\d{2})?)", "food"),
|
102 |
+
(r"utilities:?\s*\$?(\d+(?:,\d{3})*(?:\.\d{2})?)", "utilities"),
|
103 |
+
(
|
104 |
+
r"transportation:?\s*\$?(\d+(?:,\d{3})*(?:\.\d{2})?)",
|
105 |
+
"transportation",
|
106 |
+
),
|
107 |
+
]
|
108 |
+
|
109 |
+
for pattern, category in expense_patterns:
|
110 |
+
match = re.search(pattern, message, re.I)
|
111 |
+
if match:
|
112 |
+
expenses[category] = float(match.group(1).replace(",", ""))
|
113 |
+
|
114 |
+
return json.dumps({"income": income, "expenses": expenses})
|
115 |
+
|
116 |
+
elif tool_name == "portfolio_analyzer":
|
117 |
+
# Try to extract portfolio data
|
118 |
+
if "holdings" in message:
|
119 |
+
return message # Assume it's already formatted
|
120 |
+
return json.dumps({"holdings": [{"symbol": "AAPL", "shares": 100}]})
|
121 |
+
|
122 |
+
elif tool_name == "market_trends":
|
123 |
+
return message
|
124 |
+
|
125 |
+
return message
|
126 |
+
|
127 |
+
def process_message_with_details(
|
128 |
+
self, message: str, history: List[dict] = None
|
129 |
+
) -> Tuple[str, str, str, List[str], List[str]]:
|
130 |
+
"""Process a message and return response, tool used, tool result, and all tools/results"""
|
131 |
+
if history is None:
|
132 |
+
history = []
|
133 |
+
|
134 |
+
# Check if this is a multi-tool query (contains keywords for multiple tools)
|
135 |
+
message_lower = message.lower()
|
136 |
+
tool_keywords = {
|
137 |
+
"budget_planner": ["budget", "income", "expense", "spending", "allocat"],
|
138 |
+
"investment_analyzer": ["stock", "invest", "buy", "sell", "analyze"],
|
139 |
+
"portfolio_analyzer": ["portfolio", "holdings", "allocation", "diversif"],
|
140 |
+
"market_trends": ["market", "trend", "news", "sector", "economic"],
|
141 |
+
"expense_tracker": ["track", "expense", "spending", "categoriz"]
|
142 |
+
}
|
143 |
+
|
144 |
+
detected_tools = []
|
145 |
+
for tool_name, keywords in tool_keywords.items():
|
146 |
+
if any(word in message_lower for word in keywords):
|
147 |
+
# Special check for investment analyzer - needs stock symbols
|
148 |
+
if tool_name == "investment_analyzer":
|
149 |
+
if re.search(r"\b[A-Z]{2,5}\b", message) or any(word in message_lower for word in ["stock", "invest", "recommend"]):
|
150 |
+
detected_tools.append(tool_name)
|
151 |
+
else:
|
152 |
+
detected_tools.append(tool_name)
|
153 |
+
|
154 |
+
# If multiple tools detected or complex query, use agent executor
|
155 |
+
if len(detected_tools) > 1 or len(message.split()) > 15:
|
156 |
+
try:
|
157 |
+
result = self.agent_executor.invoke({"input": message, "messages": []})
|
158 |
+
|
159 |
+
tool_used, tool_result, all_tools, all_results = self._extract_tool_usage(
|
160 |
+
result.get("intermediate_steps", [])
|
161 |
+
)
|
162 |
+
return result["output"], tool_used, tool_result, all_tools, all_results
|
163 |
+
|
164 |
+
except Exception as e:
|
165 |
+
return (
|
166 |
+
f"I encountered an error processing your request: {str(e)}",
|
167 |
+
None,
|
168 |
+
None,
|
169 |
+
[],
|
170 |
+
[]
|
171 |
+
)
|
172 |
+
|
173 |
+
# Single tool execution for simple queries
|
174 |
+
elif len(detected_tools) == 1:
|
175 |
+
selected_tool = detected_tools[0]
|
176 |
+
try:
|
177 |
+
tool = self.tools_by_name[selected_tool]
|
178 |
+
tool_input = self._prepare_tool_input(message, selected_tool)
|
179 |
+
|
180 |
+
# Execute the tool
|
181 |
+
tool_result = tool.func(tool_input)
|
182 |
+
|
183 |
+
# Generate response based on tool result - optimized for speed
|
184 |
+
response_prompt = f"""Based on this {selected_tool.replace('_', ' ')} analysis, provide a concise financial summary for: {message}
|
185 |
+
|
186 |
+
Data: {tool_result}
|
187 |
+
|
188 |
+
Keep response under 200 words with key insights and 2-3 actionable recommendations."""
|
189 |
+
|
190 |
+
response = self.llm.invoke(
|
191 |
+
[
|
192 |
+
SystemMessage(content="Financial advisor. Be concise and actionable."),
|
193 |
+
HumanMessage(content=response_prompt),
|
194 |
+
]
|
195 |
+
)
|
196 |
+
|
197 |
+
return response.content, selected_tool, tool_result, [selected_tool], [tool_result]
|
198 |
+
|
199 |
+
except Exception as e:
|
200 |
+
return f"Error using {selected_tool}: {str(e)}", selected_tool, None, [], []
|
201 |
+
|
202 |
+
# Fallback to agent executor for unclear queries
|
203 |
+
else:
|
204 |
+
try:
|
205 |
+
result = self.agent_executor.invoke({"input": message, "messages": []})
|
206 |
+
|
207 |
+
tool_used, tool_result, all_tools, all_results = self._extract_tool_usage(
|
208 |
+
result.get("intermediate_steps", [])
|
209 |
+
)
|
210 |
+
return result["output"], tool_used, tool_result, all_tools, all_results
|
211 |
+
|
212 |
+
except Exception as e:
|
213 |
+
return (
|
214 |
+
f"I encountered an error processing your request: {str(e)}",
|
215 |
+
None,
|
216 |
+
None,
|
217 |
+
[],
|
218 |
+
[]
|
219 |
+
)
|
220 |
+
|
221 |
+
def process_message(self, message: str, history: List[dict] = None):
|
222 |
+
"""Process a user message and return response"""
|
223 |
+
response, _, _, _, _ = self.process_message_with_details(message, history)
|
224 |
+
return response
|
225 |
+
|
226 |
+
def stream_response(self, message: str, tool_result: str, selected_tool: str, response_type: str = "short"):
|
227 |
+
"""Stream the LLM response in real-time"""
|
228 |
+
|
229 |
+
if response_type == "detailed":
|
230 |
+
response_prompt = f"""Based on the following comprehensive analysis from the {selected_tool.replace('_', ' ').title()}:
|
231 |
+
|
232 |
+
{tool_result}
|
233 |
+
|
234 |
+
Provide detailed financial advice to the user addressing their question: {message}
|
235 |
+
|
236 |
+
Guidelines:
|
237 |
+
- Be thorough and comprehensive
|
238 |
+
- Reference specific data points from the analysis
|
239 |
+
- Provide clear, actionable recommendations with explanations
|
240 |
+
- Include multiple scenarios or considerations where relevant
|
241 |
+
- Use a professional but friendly tone
|
242 |
+
- Structure your response with clear sections
|
243 |
+
- Provide context for your recommendations"""
|
244 |
+
|
245 |
+
system_message = "You are a professional financial advisor. Provide comprehensive, detailed advice based on the analysis results. Be thorough and educational."
|
246 |
+
else:
|
247 |
+
response_prompt = f"""Based on this {selected_tool.replace('_', ' ')} analysis, provide a concise financial summary for: {message}
|
248 |
+
|
249 |
+
Data: {tool_result}
|
250 |
+
|
251 |
+
Keep response under 200 words with key insights and 2-3 actionable recommendations."""
|
252 |
+
|
253 |
+
system_message = "Financial advisor. Be concise and actionable."
|
254 |
+
|
255 |
+
messages = [
|
256 |
+
SystemMessage(content=system_message),
|
257 |
+
HumanMessage(content=response_prompt),
|
258 |
+
]
|
259 |
+
|
260 |
+
# Stream the response token by token
|
261 |
+
for chunk in self.llm.stream(messages):
|
262 |
+
if chunk.content:
|
263 |
+
yield chunk.content
|
agents/tools.py
ADDED
@@ -0,0 +1,696 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import json
|
2 |
+
from datetime import datetime, timedelta
|
3 |
+
from typing import List, Dict, Any
|
4 |
+
|
5 |
+
import pandas as pd
|
6 |
+
import yfinance as yf
|
7 |
+
from langchain.tools import Tool
|
8 |
+
from langchain_community.tools.tavily_search import TavilySearchResults
|
9 |
+
|
10 |
+
|
11 |
+
class FinancialTools:
|
12 |
+
def __init__(self, tavily_api_key: str):
|
13 |
+
self.tavily_search = TavilySearchResults(api_key=tavily_api_key)
|
14 |
+
|
15 |
+
def create_budget_planner(self) -> Tool:
|
16 |
+
def budget_planner(input_str: str) -> str:
|
17 |
+
"""Create a personalized budget plan with advanced features"""
|
18 |
+
try:
|
19 |
+
data = json.loads(input_str)
|
20 |
+
income = data.get("income", 0)
|
21 |
+
expenses = data.get("expenses", {})
|
22 |
+
goals = data.get("savings_goals", {})
|
23 |
+
debt = data.get("debt", {})
|
24 |
+
|
25 |
+
# Calculate budget allocations using 50/30/20 rule
|
26 |
+
needs = income * 0.5
|
27 |
+
wants = income * 0.3
|
28 |
+
savings = income * 0.2
|
29 |
+
|
30 |
+
total_expenses = sum(expenses.values())
|
31 |
+
remaining = income - total_expenses
|
32 |
+
|
33 |
+
# Debt analysis
|
34 |
+
total_debt = sum(debt.values()) if debt else 0
|
35 |
+
debt_to_income = (total_debt / income * 100) if income > 0 else 0
|
36 |
+
|
37 |
+
# Emergency fund calculation (3-6 months of expenses)
|
38 |
+
emergency_fund_needed = total_expenses * 6
|
39 |
+
emergency_fund_goal = goals.get("emergency_fund", 0)
|
40 |
+
|
41 |
+
# Calculate actual savings potential
|
42 |
+
debt_payments = debt.get("monthly_payments", 0)
|
43 |
+
available_for_savings = remaining - debt_payments
|
44 |
+
|
45 |
+
budget_plan = {
|
46 |
+
"monthly_income": income,
|
47 |
+
"recommended_allocation": {
|
48 |
+
"needs": needs,
|
49 |
+
"wants": wants,
|
50 |
+
"savings": savings,
|
51 |
+
},
|
52 |
+
"current_expenses": expenses,
|
53 |
+
"total_expenses": total_expenses,
|
54 |
+
"remaining_budget": remaining,
|
55 |
+
"savings_rate": (available_for_savings / income * 100) if income > 0 else 0,
|
56 |
+
"debt_analysis": {
|
57 |
+
"total_debt": total_debt,
|
58 |
+
"debt_to_income_ratio": debt_to_income,
|
59 |
+
"monthly_payments": debt_payments,
|
60 |
+
},
|
61 |
+
"emergency_fund": {
|
62 |
+
"recommended": emergency_fund_needed,
|
63 |
+
"current": emergency_fund_goal,
|
64 |
+
"progress": (emergency_fund_goal / emergency_fund_needed * 100) if emergency_fund_needed > 0 else 0,
|
65 |
+
},
|
66 |
+
"savings_optimization": {
|
67 |
+
"available_monthly": available_for_savings,
|
68 |
+
"annual_savings_potential": available_for_savings * 12,
|
69 |
+
},
|
70 |
+
"recommendations": [],
|
71 |
+
}
|
72 |
+
|
73 |
+
# Enhanced recommendations
|
74 |
+
if available_for_savings < savings:
|
75 |
+
budget_plan["recommendations"].append(
|
76 |
+
f"Increase savings by ${savings - available_for_savings:.2f}/month to reach 20% goal"
|
77 |
+
)
|
78 |
+
|
79 |
+
if debt_to_income > 36:
|
80 |
+
budget_plan["recommendations"].append(
|
81 |
+
f"High debt-to-income ratio ({debt_to_income:.1f}%). Consider debt consolidation."
|
82 |
+
)
|
83 |
+
|
84 |
+
if emergency_fund_goal < emergency_fund_needed:
|
85 |
+
monthly_needed = (emergency_fund_needed - emergency_fund_goal) / 12
|
86 |
+
budget_plan["recommendations"].append(
|
87 |
+
f"Build emergency fund: save ${monthly_needed:.2f}/month for 12 months"
|
88 |
+
)
|
89 |
+
|
90 |
+
# Expense optimization suggestions
|
91 |
+
largest_expense = max(expenses.items(), key=lambda x: x[1]) if expenses else None
|
92 |
+
if largest_expense and largest_expense[1] > income * 0.35:
|
93 |
+
budget_plan["recommendations"].append(
|
94 |
+
f"Your {largest_expense[0]} expense (${largest_expense[1]:.2f}) is high. Consider cost reduction."
|
95 |
+
)
|
96 |
+
|
97 |
+
return json.dumps(budget_plan, indent=2)
|
98 |
+
except Exception as e:
|
99 |
+
return f"Error creating budget plan: {str(e)}"
|
100 |
+
|
101 |
+
return Tool(
|
102 |
+
name="budget_planner",
|
103 |
+
description="Create personalized budget plans with income and expense analysis",
|
104 |
+
func=budget_planner,
|
105 |
+
)
|
106 |
+
|
107 |
+
def create_investment_analyzer(self) -> Tool:
|
108 |
+
def investment_analyzer(symbol: str) -> str:
|
109 |
+
"""Analyze stocks with advanced metrics, sector comparison, and risk assessment"""
|
110 |
+
try:
|
111 |
+
stock = yf.Ticker(symbol.upper())
|
112 |
+
info = stock.info
|
113 |
+
hist = stock.history(period="1y") # Reduced from 2y to 1y for speed
|
114 |
+
|
115 |
+
if hist.empty:
|
116 |
+
return f"No data available for {symbol}"
|
117 |
+
|
118 |
+
# Calculate key metrics
|
119 |
+
current_price = info.get("currentPrice", hist["Close"].iloc[-1])
|
120 |
+
pe_ratio = info.get("trailingPE", "N/A")
|
121 |
+
pb_ratio = info.get("priceToBook", "N/A")
|
122 |
+
dividend_yield = (info.get("dividendYield", 0) * 100 if info.get("dividendYield") else 0)
|
123 |
+
market_cap = info.get("marketCap", "N/A")
|
124 |
+
beta = info.get("beta", "N/A")
|
125 |
+
sector = info.get("sector", "Unknown")
|
126 |
+
industry = info.get("industry", "Unknown")
|
127 |
+
|
128 |
+
# Advanced technical indicators
|
129 |
+
sma_20 = hist["Close"].rolling(window=20).mean().iloc[-1]
|
130 |
+
sma_50 = hist["Close"].rolling(window=50).mean().iloc[-1] if len(hist) >= 50 else None
|
131 |
+
sma_200 = hist["Close"].rolling(window=200).mean().iloc[-1] if len(hist) >= 200 else None
|
132 |
+
|
133 |
+
# RSI calculation
|
134 |
+
delta = hist["Close"].diff()
|
135 |
+
gain = (delta.where(delta > 0, 0)).rolling(window=14).mean()
|
136 |
+
loss = (-delta.where(delta < 0, 0)).rolling(window=14).mean()
|
137 |
+
rs = gain / loss
|
138 |
+
rsi = 100 - (100 / (1 + rs)).iloc[-1]
|
139 |
+
|
140 |
+
# Simplified MACD calculation
|
141 |
+
ema_12 = hist["Close"].ewm(span=12).mean()
|
142 |
+
ema_26 = hist["Close"].ewm(span=26).mean()
|
143 |
+
macd = ema_12 - ema_26
|
144 |
+
macd_signal = macd.ewm(span=9).mean()
|
145 |
+
|
146 |
+
# Simplified Bollinger Bands (only what we need)
|
147 |
+
bb_middle = hist["Close"].rolling(window=20).mean()
|
148 |
+
bb_std_dev = hist["Close"].rolling(window=20).std()
|
149 |
+
bb_upper = bb_middle + (bb_std_dev * 2)
|
150 |
+
bb_lower = bb_middle - (bb_std_dev * 2)
|
151 |
+
|
152 |
+
# Simplified volatility analysis
|
153 |
+
volatility_30d = hist["Close"].pct_change().rolling(30).std().iloc[-1] * 100
|
154 |
+
|
155 |
+
# Value at Risk (VaR) - 5% level
|
156 |
+
returns = hist["Close"].pct_change().dropna()
|
157 |
+
var_5 = returns.quantile(0.05) * 100
|
158 |
+
|
159 |
+
# Performance metrics
|
160 |
+
price_1m = hist["Close"].iloc[-22] if len(hist) >= 22 else None
|
161 |
+
price_3m = hist["Close"].iloc[-66] if len(hist) >= 66 else None
|
162 |
+
price_6m = hist["Close"].iloc[-132] if len(hist) >= 132 else None
|
163 |
+
price_1y = hist["Close"].iloc[-252] if len(hist) >= 252 else None
|
164 |
+
|
165 |
+
performance = {}
|
166 |
+
if price_1m: performance["1_month"] = ((current_price - price_1m) / price_1m * 100)
|
167 |
+
if price_3m: performance["3_month"] = ((current_price - price_3m) / price_3m * 100)
|
168 |
+
if price_6m: performance["6_month"] = ((current_price - price_6m) / price_6m * 100)
|
169 |
+
if price_1y: performance["1_year"] = ((current_price - price_1y) / price_1y * 100)
|
170 |
+
|
171 |
+
# Sharpe ratio calculation (using risk-free rate of 4%)
|
172 |
+
risk_free_rate = 0.04
|
173 |
+
mean_return = returns.mean() * 252
|
174 |
+
return_std = returns.std() * (252**0.5)
|
175 |
+
sharpe_ratio = (mean_return - risk_free_rate) / return_std if return_std > 0 else 0
|
176 |
+
|
177 |
+
# Risk assessment
|
178 |
+
risk_score = 0
|
179 |
+
risk_factors = []
|
180 |
+
|
181 |
+
if volatility_30d > 30:
|
182 |
+
risk_score += 2
|
183 |
+
risk_factors.append("High volatility (>30%)")
|
184 |
+
elif volatility_30d > 20:
|
185 |
+
risk_score += 1
|
186 |
+
risk_factors.append("Moderate volatility (20-30%)")
|
187 |
+
|
188 |
+
if isinstance(beta, (int, float)):
|
189 |
+
if beta > 1.5:
|
190 |
+
risk_score += 2
|
191 |
+
risk_factors.append(f"High beta ({beta:.2f}) - market sensitive")
|
192 |
+
elif beta > 1.2:
|
193 |
+
risk_score += 1
|
194 |
+
risk_factors.append(f"Above-average beta ({beta:.2f})")
|
195 |
+
|
196 |
+
if var_5 < -5:
|
197 |
+
risk_score += 2
|
198 |
+
risk_factors.append(f"High downside risk (VaR: {var_5:.1f}%)")
|
199 |
+
|
200 |
+
# Enhanced recommendation logic
|
201 |
+
recommendation = "HOLD"
|
202 |
+
confidence = 50
|
203 |
+
reasoning = []
|
204 |
+
|
205 |
+
# Technical analysis
|
206 |
+
if current_price < bb_lower.iloc[-1]:
|
207 |
+
recommendation = "BUY"
|
208 |
+
confidence += 20
|
209 |
+
reasoning.append("Price below Bollinger Band lower bound (oversold)")
|
210 |
+
elif current_price > bb_upper.iloc[-1]:
|
211 |
+
recommendation = "SELL"
|
212 |
+
confidence += 15
|
213 |
+
reasoning.append("Price above Bollinger Band upper bound (overbought)")
|
214 |
+
|
215 |
+
# RSI analysis
|
216 |
+
if rsi < 30:
|
217 |
+
if recommendation != "SELL":
|
218 |
+
recommendation = "BUY"
|
219 |
+
confidence += 15
|
220 |
+
reasoning.append(f"RSI oversold ({rsi:.1f})")
|
221 |
+
elif rsi > 70:
|
222 |
+
if recommendation != "BUY":
|
223 |
+
recommendation = "SELL"
|
224 |
+
confidence += 10
|
225 |
+
reasoning.append(f"RSI overbought ({rsi:.1f})")
|
226 |
+
|
227 |
+
# MACD analysis
|
228 |
+
if macd.iloc[-1] > macd_signal.iloc[-1] and macd.iloc[-2] <= macd_signal.iloc[-2]:
|
229 |
+
if recommendation != "SELL":
|
230 |
+
recommendation = "BUY"
|
231 |
+
confidence += 10
|
232 |
+
reasoning.append("MACD bullish crossover")
|
233 |
+
|
234 |
+
# Fundamental analysis
|
235 |
+
if isinstance(pe_ratio, (int, float)):
|
236 |
+
if pe_ratio < 15:
|
237 |
+
confidence += 10
|
238 |
+
reasoning.append("Low P/E ratio suggests undervaluation")
|
239 |
+
elif pe_ratio > 30:
|
240 |
+
confidence -= 5
|
241 |
+
reasoning.append("High P/E ratio suggests overvaluation")
|
242 |
+
|
243 |
+
# Risk adjustment
|
244 |
+
if risk_score >= 4:
|
245 |
+
if recommendation == "BUY":
|
246 |
+
recommendation = "HOLD"
|
247 |
+
confidence -= 15
|
248 |
+
reasoning.append("High risk profile suggests caution")
|
249 |
+
|
250 |
+
analysis = {
|
251 |
+
"symbol": symbol.upper(),
|
252 |
+
"company_name": info.get("longName", symbol),
|
253 |
+
"sector": sector,
|
254 |
+
"industry": industry,
|
255 |
+
"current_price": f"${current_price:.2f}",
|
256 |
+
"market_cap": f"${market_cap:,.0f}" if isinstance(market_cap, (int, float)) else "N/A",
|
257 |
+
"fundamental_metrics": {
|
258 |
+
"pe_ratio": pe_ratio,
|
259 |
+
"pb_ratio": pb_ratio,
|
260 |
+
"dividend_yield": f"{dividend_yield:.2f}%",
|
261 |
+
"beta": beta,
|
262 |
+
"sharpe_ratio": f"{sharpe_ratio:.2f}",
|
263 |
+
},
|
264 |
+
"technical_indicators": {
|
265 |
+
"sma_20": f"${sma_20:.2f}",
|
266 |
+
"sma_50": f"${sma_50:.2f}" if sma_50 else "N/A",
|
267 |
+
"sma_200": f"${sma_200:.2f}" if sma_200 else "N/A",
|
268 |
+
"rsi": f"{rsi:.1f}",
|
269 |
+
"macd": f"{macd.iloc[-1]:.2f}",
|
270 |
+
"bollinger_position": "Lower" if current_price < bb_lower.iloc[-1] else "Upper" if current_price > bb_upper.iloc[-1] else "Middle",
|
271 |
+
},
|
272 |
+
"risk_assessment": {
|
273 |
+
"volatility_30d": f"{volatility_30d:.1f}%",
|
274 |
+
"value_at_risk_5%": f"{var_5:.1f}%",
|
275 |
+
"risk_score": f"{risk_score}/6",
|
276 |
+
"risk_factors": risk_factors,
|
277 |
+
"risk_level": "Low" if risk_score <= 1 else "Medium" if risk_score <= 3 else "High",
|
278 |
+
},
|
279 |
+
"price_levels": {
|
280 |
+
"52_week_high": f"${info.get('fiftyTwoWeekHigh', 'N/A')}",
|
281 |
+
"52_week_low": f"${info.get('fiftyTwoWeekLow', 'N/A')}",
|
282 |
+
},
|
283 |
+
"performance": {k: f"{v:.1f}%" for k, v in performance.items()},
|
284 |
+
"recommendation": {
|
285 |
+
"action": recommendation,
|
286 |
+
"confidence": f"{min(max(confidence, 20), 95)}%",
|
287 |
+
"reasoning": reasoning,
|
288 |
+
"target_allocation": "5-10%" if recommendation == "BUY" else "0-5%" if recommendation == "SELL" else "3-7%",
|
289 |
+
},
|
290 |
+
"last_updated": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
291 |
+
}
|
292 |
+
|
293 |
+
return json.dumps(analysis, indent=2)
|
294 |
+
except Exception as e:
|
295 |
+
return f"Error analyzing {symbol}: {str(e)}"
|
296 |
+
|
297 |
+
return Tool(
|
298 |
+
name="investment_analyzer",
|
299 |
+
description="Analyze stocks and provide investment recommendations",
|
300 |
+
func=investment_analyzer,
|
301 |
+
)
|
302 |
+
|
303 |
+
def create_expense_tracker(self) -> Tool:
|
304 |
+
def expense_tracker(input_str: str) -> str:
|
305 |
+
"""Track and analyze expenses with advanced insights, trends, and predictions"""
|
306 |
+
try:
|
307 |
+
data = json.loads(input_str)
|
308 |
+
expenses = data.get("expenses", [])
|
309 |
+
historical_data = data.get("historical_expenses", [])
|
310 |
+
budget_limits = data.get("budget_limits", {})
|
311 |
+
|
312 |
+
# Combine current and historical data
|
313 |
+
all_expenses = expenses + historical_data
|
314 |
+
df = pd.DataFrame(all_expenses)
|
315 |
+
|
316 |
+
if df.empty:
|
317 |
+
return "No expense data provided"
|
318 |
+
|
319 |
+
# Ensure date column exists and is properly formatted
|
320 |
+
if "date" in df.columns:
|
321 |
+
df["date"] = pd.to_datetime(df["date"])
|
322 |
+
df = df.sort_values("date")
|
323 |
+
else:
|
324 |
+
# Add current date for expenses without dates
|
325 |
+
df["date"] = datetime.now()
|
326 |
+
|
327 |
+
# Current period analysis
|
328 |
+
current_df = pd.DataFrame(expenses) if expenses else pd.DataFrame()
|
329 |
+
total_current = current_df["amount"].sum() if not current_df.empty else 0
|
330 |
+
|
331 |
+
# Category analysis
|
332 |
+
category_summary = df.groupby("category")["amount"].agg(["sum", "mean", "count", "std"]).to_dict("index")
|
333 |
+
|
334 |
+
# Trend analysis (if historical data available)
|
335 |
+
trends = {}
|
336 |
+
predictions = {}
|
337 |
+
|
338 |
+
if len(df) > 1 and "date" in df.columns:
|
339 |
+
# Monthly spending trends
|
340 |
+
df["month"] = df["date"].dt.to_period("M")
|
341 |
+
monthly_spending = df.groupby("month")["amount"].sum()
|
342 |
+
|
343 |
+
if len(monthly_spending) > 1:
|
344 |
+
# Calculate month-over-month growth
|
345 |
+
mom_growth = monthly_spending.pct_change().iloc[-1] * 100
|
346 |
+
trends["monthly_growth"] = f"{mom_growth:.1f}%"
|
347 |
+
|
348 |
+
# Simple linear trend prediction for next month
|
349 |
+
if len(monthly_spending) >= 3:
|
350 |
+
recent_trend = monthly_spending.tail(3).mean()
|
351 |
+
predictions["next_month_estimate"] = f"${recent_trend:.2f}"
|
352 |
+
|
353 |
+
# Category trends
|
354 |
+
for category in df["category"].unique():
|
355 |
+
cat_data = df[df["category"] == category].groupby("month")["amount"].sum()
|
356 |
+
if len(cat_data) > 1:
|
357 |
+
cat_trend = cat_data.pct_change().iloc[-1] * 100
|
358 |
+
trends[f"{category}_trend"] = f"{cat_trend:.1f}%"
|
359 |
+
|
360 |
+
# Spending pattern analysis
|
361 |
+
if "date" in df.columns:
|
362 |
+
df["day_of_week"] = df["date"].dt.day_name()
|
363 |
+
df["hour"] = df["date"].dt.hour
|
364 |
+
|
365 |
+
spending_patterns = {
|
366 |
+
"busiest_day": df.groupby("day_of_week")["amount"].sum().idxmax(),
|
367 |
+
"peak_spending_hour": df.groupby("hour")["amount"].sum().idxmax(),
|
368 |
+
}
|
369 |
+
else:
|
370 |
+
spending_patterns = {}
|
371 |
+
|
372 |
+
# Budget analysis
|
373 |
+
budget_analysis = {}
|
374 |
+
if budget_limits:
|
375 |
+
for category, limit in budget_limits.items():
|
376 |
+
cat_spending = category_summary.get(category, {}).get("sum", 0)
|
377 |
+
budget_analysis[category] = {
|
378 |
+
"limit": f"${limit:.2f}",
|
379 |
+
"spent": f"${cat_spending:.2f}",
|
380 |
+
"remaining": f"${max(0, limit - cat_spending):.2f}",
|
381 |
+
"percentage_used": f"{(cat_spending / limit * 100):.1f}%" if limit > 0 else "N/A",
|
382 |
+
"status": "Over Budget" if cat_spending > limit else "Within Budget"
|
383 |
+
}
|
384 |
+
|
385 |
+
# Anomaly detection (expenses significantly above average)
|
386 |
+
anomalies = []
|
387 |
+
if not df.empty:
|
388 |
+
for category in df["category"].unique():
|
389 |
+
cat_data = df[df["category"] == category]["amount"]
|
390 |
+
if len(cat_data) > 2:
|
391 |
+
mean_spending = cat_data.mean()
|
392 |
+
std_spending = cat_data.std()
|
393 |
+
threshold = mean_spending + (2 * std_spending)
|
394 |
+
|
395 |
+
recent_anomalies = cat_data[cat_data > threshold]
|
396 |
+
if not recent_anomalies.empty:
|
397 |
+
anomalies.append({
|
398 |
+
"category": category,
|
399 |
+
"unusual_amount": f"${recent_anomalies.iloc[-1]:.2f}",
|
400 |
+
"typical_range": f"${mean_spending:.2f} Β± ${std_spending:.2f}"
|
401 |
+
})
|
402 |
+
|
403 |
+
# Generate insights and recommendations
|
404 |
+
insights = []
|
405 |
+
recommendations = []
|
406 |
+
|
407 |
+
# Top spending categories
|
408 |
+
top_categories = sorted(category_summary.items(), key=lambda x: x[1]["sum"], reverse=True)[:3]
|
409 |
+
category_strings = [f'{cat} (${data["sum"]:.2f})' for cat, data in top_categories]
|
410 |
+
insights.append(f"Top 3 spending categories: {', '.join(category_strings)}")
|
411 |
+
|
412 |
+
# Spending frequency analysis
|
413 |
+
if not df.empty:
|
414 |
+
avg_transaction = df["amount"].mean()
|
415 |
+
insights.append(f"Average transaction: ${avg_transaction:.2f}")
|
416 |
+
|
417 |
+
if avg_transaction > 100:
|
418 |
+
recommendations.append("Consider breaking down large expenses into smaller, more frequent transactions for better budget control")
|
419 |
+
|
420 |
+
# Trend-based recommendations
|
421 |
+
if "monthly_growth" in trends:
|
422 |
+
growth = float(trends["monthly_growth"].rstrip("%"))
|
423 |
+
if growth > 10:
|
424 |
+
recommendations.append(f"Spending increased {growth:.1f}% this month. Review discretionary expenses.")
|
425 |
+
elif growth < -10:
|
426 |
+
recommendations.append(f"Good job! Spending decreased {abs(growth):.1f}% this month.")
|
427 |
+
|
428 |
+
# Budget recommendations
|
429 |
+
for category, analysis in budget_analysis.items():
|
430 |
+
if "Over Budget" in analysis["status"]:
|
431 |
+
recommendations.append(f"Reduce {category} spending - currently over budget")
|
432 |
+
elif float(analysis["percentage_used"].rstrip("%")) > 80:
|
433 |
+
recommendations.append(f"Approaching {category} budget limit ({analysis['percentage_used']})")
|
434 |
+
|
435 |
+
analysis_result = {
|
436 |
+
"current_period": {
|
437 |
+
"total_expenses": f"${total_current:.2f}",
|
438 |
+
"transaction_count": len(current_df) if not current_df.empty else 0,
|
439 |
+
"average_transaction": f"${(total_current / len(current_df)):.2f}" if not current_df.empty else "$0.00",
|
440 |
+
},
|
441 |
+
"category_analysis": {
|
442 |
+
category: {
|
443 |
+
"total": f"${data['sum']:.2f}",
|
444 |
+
"average": f"${data['mean']:.2f}",
|
445 |
+
"transactions": int(data['count']),
|
446 |
+
"variability": f"${data.get('std', 0):.2f}"
|
447 |
+
} for category, data in category_summary.items()
|
448 |
+
},
|
449 |
+
"trends": trends,
|
450 |
+
"predictions": predictions,
|
451 |
+
"spending_patterns": spending_patterns,
|
452 |
+
"budget_analysis": budget_analysis,
|
453 |
+
"anomalies": anomalies,
|
454 |
+
"insights": insights,
|
455 |
+
"recommendations": recommendations,
|
456 |
+
"analysis_date": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
457 |
+
}
|
458 |
+
|
459 |
+
return json.dumps(analysis_result, indent=2)
|
460 |
+
|
461 |
+
except Exception as e:
|
462 |
+
return f"Error tracking expenses: {str(e)}"
|
463 |
+
|
464 |
+
return Tool(
|
465 |
+
name="expense_tracker",
|
466 |
+
description="Track and analyze expenses with detailed insights",
|
467 |
+
func=expense_tracker,
|
468 |
+
)
|
469 |
+
|
470 |
+
def create_market_trends_analyzer(self) -> Tool:
|
471 |
+
def market_trends(query: str) -> str:
|
472 |
+
"""Get comprehensive real-time market trends, news, and sector analysis"""
|
473 |
+
try:
|
474 |
+
# Get current year for search queries
|
475 |
+
current_year = datetime.now().year
|
476 |
+
|
477 |
+
# Status tracking for API calls
|
478 |
+
status_updates = []
|
479 |
+
|
480 |
+
# Optimized single comprehensive search instead of multiple calls
|
481 |
+
comprehensive_query = f"stock market {query} trends analysis financial news {current_year} latest"
|
482 |
+
|
483 |
+
# Get primary market information
|
484 |
+
status_updates.append("π Fetching latest market news via Tavily Search API...")
|
485 |
+
market_news = self.tavily_search.run(comprehensive_query)
|
486 |
+
status_updates.append("β
Market news retrieved successfully")
|
487 |
+
|
488 |
+
# Quick market indices check (reduced to just S&P 500 and NASDAQ for speed)
|
489 |
+
index_data = {}
|
490 |
+
market_sentiment = {"overall": "Unknown", "note": "Limited data"}
|
491 |
+
|
492 |
+
try:
|
493 |
+
status_updates.append("π Fetching market indices via Yahoo Finance API...")
|
494 |
+
# Fetch only key indices for speed
|
495 |
+
key_indices = ["^GSPC", "^IXIC"] # S&P 500, NASDAQ
|
496 |
+
|
497 |
+
for index in key_indices:
|
498 |
+
index_names = {"^GSPC": "S&P 500", "^IXIC": "NASDAQ"}
|
499 |
+
status_updates.append(f"π Getting {index_names[index]} data...")
|
500 |
+
|
501 |
+
ticker = yf.Ticker(index)
|
502 |
+
hist = ticker.history(period="2d") # Reduced period for speed
|
503 |
+
if not hist.empty:
|
504 |
+
current = hist["Close"].iloc[-1]
|
505 |
+
prev = hist["Close"].iloc[-2] if len(hist) > 1 else current
|
506 |
+
change = ((current - prev) / prev * 100) if prev != 0 else 0
|
507 |
+
|
508 |
+
index_data[index_names[index]] = {
|
509 |
+
"current": round(current, 2),
|
510 |
+
"change_pct": round(change, 2),
|
511 |
+
"direction": "π" if change > 0 else "π" if change < 0 else "β‘οΈ"
|
512 |
+
}
|
513 |
+
|
514 |
+
status_updates.append("β
Market indices data retrieved successfully")
|
515 |
+
|
516 |
+
# Simple sentiment based on available indices
|
517 |
+
if index_data:
|
518 |
+
status_updates.append("π§ Analyzing market sentiment...")
|
519 |
+
positive_count = sum(1 for data in index_data.values() if data["change_pct"] > 0)
|
520 |
+
total_count = len(index_data)
|
521 |
+
|
522 |
+
if positive_count >= total_count * 0.75:
|
523 |
+
sentiment = "π’ Bullish"
|
524 |
+
elif positive_count <= total_count * 0.25:
|
525 |
+
sentiment = "π΄ Bearish"
|
526 |
+
else:
|
527 |
+
sentiment = "π‘ Mixed"
|
528 |
+
|
529 |
+
market_sentiment = {
|
530 |
+
"overall": sentiment,
|
531 |
+
"summary": f"{positive_count}/{total_count} indices positive"
|
532 |
+
}
|
533 |
+
status_updates.append("β
Market sentiment analysis completed")
|
534 |
+
|
535 |
+
except Exception as index_error:
|
536 |
+
status_updates.append(f"β Error fetching market indices: {str(index_error)}")
|
537 |
+
index_data = {"error": f"Index data unavailable: {str(index_error)}"}
|
538 |
+
|
539 |
+
# Extract key themes from search results
|
540 |
+
status_updates.append("π Analyzing key market themes...")
|
541 |
+
key_themes = _extract_key_themes(market_news)
|
542 |
+
status_updates.append("β
Theme analysis completed")
|
543 |
+
|
544 |
+
# Format output for better readability
|
545 |
+
def format_search_results(results):
|
546 |
+
"""Convert search results to readable format"""
|
547 |
+
if isinstance(results, list):
|
548 |
+
# Extract key information from search results
|
549 |
+
formatted = []
|
550 |
+
for item in results[:3]: # Limit to top 3 results
|
551 |
+
if isinstance(item, dict):
|
552 |
+
title = item.get('title', 'No title')
|
553 |
+
content = item.get('content', item.get('snippet', 'No content'))
|
554 |
+
formatted.append(f"β’ {title}: {content[:200]}...")
|
555 |
+
else:
|
556 |
+
formatted.append(f"β’ {str(item)[:200]}...")
|
557 |
+
return "\n".join(formatted)
|
558 |
+
elif isinstance(results, str):
|
559 |
+
return results[:1000] + "..." if len(results) > 1000 else results
|
560 |
+
else:
|
561 |
+
return str(results)[:1000]
|
562 |
+
|
563 |
+
status_updates.append("π Compiling final analysis report...")
|
564 |
+
|
565 |
+
# Compile streamlined analysis
|
566 |
+
analysis = {
|
567 |
+
"query": query,
|
568 |
+
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
569 |
+
"api_execution_log": status_updates,
|
570 |
+
"market_summary": format_search_results(market_news),
|
571 |
+
"key_indices": index_data,
|
572 |
+
"market_sentiment": market_sentiment,
|
573 |
+
"key_themes": key_themes,
|
574 |
+
"note": "Real-time API status tracking enabled"
|
575 |
+
}
|
576 |
+
|
577 |
+
status_updates.append("β
Analysis report completed successfully")
|
578 |
+
|
579 |
+
return json.dumps(analysis, indent=2, ensure_ascii=False)
|
580 |
+
|
581 |
+
except Exception as e:
|
582 |
+
return f"Error fetching market analysis: {str(e)}"
|
583 |
+
|
584 |
+
def _extract_key_themes(news_text) -> list:
|
585 |
+
"""Extract key themes from market news"""
|
586 |
+
themes = []
|
587 |
+
keywords = {
|
588 |
+
"earnings": ["earnings", "quarterly results", "revenue", "profit"],
|
589 |
+
"fed_policy": ["federal reserve", "interest rates", "fed", "monetary policy"],
|
590 |
+
"inflation": ["inflation", "cpi", "price increases", "cost of living"],
|
591 |
+
"geopolitical": ["geopolitical", "war", "trade war", "sanctions"],
|
592 |
+
"technology": ["ai", "artificial intelligence", "tech stocks", "innovation"],
|
593 |
+
"recession": ["recession", "economic downturn", "market crash"],
|
594 |
+
}
|
595 |
+
|
596 |
+
# Handle both string and list inputs
|
597 |
+
if isinstance(news_text, list):
|
598 |
+
# Convert list to string
|
599 |
+
news_text = " ".join(str(item) for item in news_text)
|
600 |
+
elif not isinstance(news_text, str):
|
601 |
+
# Convert other types to string
|
602 |
+
news_text = str(news_text)
|
603 |
+
|
604 |
+
news_lower = news_text.lower()
|
605 |
+
for theme, terms in keywords.items():
|
606 |
+
if any(term in news_lower for term in terms):
|
607 |
+
themes.append(theme.replace("_", " ").title())
|
608 |
+
|
609 |
+
return themes[:5] # Return top 5 themes
|
610 |
+
|
611 |
+
return Tool(
|
612 |
+
name="market_trends",
|
613 |
+
description="Get real-time market trends and financial news",
|
614 |
+
func=market_trends,
|
615 |
+
)
|
616 |
+
|
617 |
+
def create_portfolio_analyzer(self) -> Tool:
|
618 |
+
def portfolio_analyzer(input_str: str) -> str:
|
619 |
+
"""Analyze portfolio performance and diversification"""
|
620 |
+
try:
|
621 |
+
data = json.loads(input_str)
|
622 |
+
holdings = data.get("holdings", [])
|
623 |
+
|
624 |
+
total_value = 0
|
625 |
+
portfolio_data = []
|
626 |
+
|
627 |
+
for holding in holdings:
|
628 |
+
symbol = holding["symbol"]
|
629 |
+
shares = holding["shares"]
|
630 |
+
|
631 |
+
stock = yf.Ticker(symbol)
|
632 |
+
current_price = stock.info.get("currentPrice", 0)
|
633 |
+
value = current_price * shares
|
634 |
+
total_value += value
|
635 |
+
|
636 |
+
portfolio_data.append(
|
637 |
+
{
|
638 |
+
"symbol": symbol,
|
639 |
+
"shares": shares,
|
640 |
+
"current_price": current_price,
|
641 |
+
"value": value,
|
642 |
+
"sector": stock.info.get("sector", "Unknown"),
|
643 |
+
}
|
644 |
+
)
|
645 |
+
|
646 |
+
# Calculate allocations
|
647 |
+
for item in portfolio_data:
|
648 |
+
item["allocation"] = (
|
649 |
+
(item["value"] / total_value * 100) if total_value > 0 else 0
|
650 |
+
)
|
651 |
+
|
652 |
+
# Sector diversification
|
653 |
+
df = pd.DataFrame(portfolio_data)
|
654 |
+
sector_allocation = df.groupby("sector")["allocation"].sum().to_dict()
|
655 |
+
|
656 |
+
analysis = {
|
657 |
+
"total_portfolio_value": f"${total_value:.2f}",
|
658 |
+
"holdings": portfolio_data,
|
659 |
+
"sector_allocation": sector_allocation,
|
660 |
+
"diversification_score": len(sector_allocation)
|
661 |
+
/ 11
|
662 |
+
* 100, # 11 major sectors
|
663 |
+
"recommendations": [],
|
664 |
+
}
|
665 |
+
|
666 |
+
# Add recommendations
|
667 |
+
if len(holdings) < 5:
|
668 |
+
analysis["recommendations"].append(
|
669 |
+
"Consider diversifying with more holdings"
|
670 |
+
)
|
671 |
+
|
672 |
+
max_allocation = max(item["allocation"] for item in portfolio_data)
|
673 |
+
if max_allocation > 30:
|
674 |
+
analysis["recommendations"].append(
|
675 |
+
f"High concentration risk: largest holding is {max_allocation:.1f}%"
|
676 |
+
)
|
677 |
+
|
678 |
+
return json.dumps(analysis, indent=2)
|
679 |
+
|
680 |
+
except Exception as e:
|
681 |
+
return f"Error analyzing portfolio: {str(e)}"
|
682 |
+
|
683 |
+
return Tool(
|
684 |
+
name="portfolio_analyzer",
|
685 |
+
description="Analyze portfolio performance and diversification",
|
686 |
+
func=portfolio_analyzer,
|
687 |
+
)
|
688 |
+
|
689 |
+
def get_all_tools(self) -> List[Tool]:
|
690 |
+
return [
|
691 |
+
self.create_budget_planner(),
|
692 |
+
self.create_investment_analyzer(),
|
693 |
+
self.create_expense_tracker(),
|
694 |
+
self.create_market_trends_analyzer(),
|
695 |
+
self.create_portfolio_analyzer(),
|
696 |
+
]
|
app.py
ADDED
@@ -0,0 +1,747 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import json
|
2 |
+
import os
|
3 |
+
import time
|
4 |
+
from pathlib import Path
|
5 |
+
|
6 |
+
import gradio as gr
|
7 |
+
from gradio import ChatMessage
|
8 |
+
|
9 |
+
from agents.financial_agent import FinancialAdvisorAgent
|
10 |
+
from agents.tools import FinancialTools
|
11 |
+
|
12 |
+
# Avatar configuration
|
13 |
+
AVATAR_IMAGES = (
|
14 |
+
None,
|
15 |
+
"./public/images/fin_logo.png",
|
16 |
+
)
|
17 |
+
|
18 |
+
# Initialize tools and agent
|
19 |
+
financial_tools = FinancialTools(tavily_api_key=os.getenv("TAVILY_API_KEY"))
|
20 |
+
tools = financial_tools.get_all_tools()
|
21 |
+
|
22 |
+
agent = FinancialAdvisorAgent(tools=tools, openai_api_key=os.getenv("OPENAI_API_KEY"))
|
23 |
+
|
24 |
+
gr.set_static_paths(paths=[(Path.cwd() / "public" / "images").absolute()])
|
25 |
+
|
26 |
+
|
27 |
+
def analyze_data_with_repl(data_type, data):
|
28 |
+
"""Analyze financial data using Python REPL with comprehensive insights"""
|
29 |
+
|
30 |
+
if data_type == "budget":
|
31 |
+
try:
|
32 |
+
budget_data = json.loads(data)
|
33 |
+
categories = list(budget_data.get("current_expenses", {}).keys())
|
34 |
+
values = list(budget_data.get("current_expenses", {}).values())
|
35 |
+
income = budget_data.get("monthly_income", budget_data.get("income", 0))
|
36 |
+
|
37 |
+
if categories and values:
|
38 |
+
total_expenses = sum(values)
|
39 |
+
analysis_text = "π° **Comprehensive Budget Analysis**\n\n"
|
40 |
+
|
41 |
+
# Income vs Expenses Overview
|
42 |
+
analysis_text += "## π **Income vs Expenses Overview**\n"
|
43 |
+
analysis_text += f"- **Monthly Income**: ${income:,.0f}\n"
|
44 |
+
analysis_text += f"- **Total Expenses**: ${total_expenses:,.0f}\n"
|
45 |
+
|
46 |
+
if income > 0:
|
47 |
+
remaining = income - total_expenses
|
48 |
+
savings_rate = (remaining / income * 100) if income > 0 else 0
|
49 |
+
|
50 |
+
if remaining > 0:
|
51 |
+
analysis_text += f"- **π Surplus**: ${remaining:,.0f}\n"
|
52 |
+
analysis_text += f"- **π Savings Rate**: {savings_rate:.1f}%\n"
|
53 |
+
else:
|
54 |
+
analysis_text += f"- **π΄ Deficit**: ${abs(remaining):,.0f}\n"
|
55 |
+
analysis_text += f"- **β οΈ Overspending**: {abs(savings_rate):.1f}%\n"
|
56 |
+
|
57 |
+
# Expense Breakdown with Progress Bars
|
58 |
+
analysis_text += "\n## π³ **Expense Breakdown**\n"
|
59 |
+
for i, (category, amount) in enumerate(zip(categories, values)):
|
60 |
+
percentage = (amount / total_expenses * 100) if total_expenses > 0 else 0
|
61 |
+
income_percentage = (amount / income * 100) if income > 0 else 0
|
62 |
+
bar = "β" * min(int(percentage / 3), 30) # Max 30 chars
|
63 |
+
|
64 |
+
analysis_text += f"**{category.title()}**: ${amount:,.0f}\n"
|
65 |
+
analysis_text += f" ββ {percentage:.1f}% of expenses | {income_percentage:.1f}% of income {bar}\n\n"
|
66 |
+
|
67 |
+
# Financial Health Metrics
|
68 |
+
analysis_text += "## π **Financial Health Metrics**\n"
|
69 |
+
avg_expense = total_expenses / len(values)
|
70 |
+
largest_expense = max(values)
|
71 |
+
smallest_expense = min(values)
|
72 |
+
largest_category = categories[values.index(largest_expense)]
|
73 |
+
smallest_category = categories[values.index(smallest_expense)]
|
74 |
+
|
75 |
+
analysis_text += f"- **Average Category Expense**: ${avg_expense:,.0f}\n"
|
76 |
+
analysis_text += f"- **Highest Expense**: {largest_category} (${largest_expense:,.0f})\n"
|
77 |
+
analysis_text += f"- **Lowest Expense**: {smallest_category} (${smallest_expense:,.0f})\n"
|
78 |
+
analysis_text += f"- **Expense Range**: ${largest_expense - smallest_expense:,.0f}\n"
|
79 |
+
|
80 |
+
# Budget Recommendations
|
81 |
+
analysis_text += "\n## π‘ **Smart Budget Insights**\n"
|
82 |
+
|
83 |
+
# 50/30/20 Rule Analysis
|
84 |
+
if income > 0:
|
85 |
+
needs_target = income * 0.50
|
86 |
+
wants_target = income * 0.30
|
87 |
+
savings_target = income * 0.20
|
88 |
+
|
89 |
+
analysis_text += f"**50/30/20 Rule Comparison:**\n"
|
90 |
+
analysis_text += f"- Needs Target (50%): ${needs_target:,.0f}\n"
|
91 |
+
analysis_text += f"- Wants Target (30%): ${wants_target:,.0f}\n"
|
92 |
+
analysis_text += f"- Savings Target (20%): ${savings_target:,.0f}\n"
|
93 |
+
|
94 |
+
if savings_rate >= 20:
|
95 |
+
analysis_text += "β
**Excellent savings rate!**\n"
|
96 |
+
elif savings_rate >= 10:
|
97 |
+
analysis_text += "β οΈ **Good savings, aim for 20%**\n"
|
98 |
+
else:
|
99 |
+
analysis_text += "π΄ **Consider reducing expenses to save more**\n"
|
100 |
+
|
101 |
+
# Category Warnings
|
102 |
+
for category, amount in zip(categories, values):
|
103 |
+
if income > 0:
|
104 |
+
cat_percentage = (amount / income * 100)
|
105 |
+
if category.lower() in ['rent', 'housing'] and cat_percentage > 30:
|
106 |
+
analysis_text += f"β οΈ **Housing costs high**: {cat_percentage:.1f}% (recommend <30%)\n"
|
107 |
+
elif category.lower() in ['food', 'dining'] and cat_percentage > 15:
|
108 |
+
analysis_text += f"β οΈ **Food costs high**: {cat_percentage:.1f}% (recommend <15%)\n"
|
109 |
+
|
110 |
+
return analysis_text
|
111 |
+
except Exception as e:
|
112 |
+
return f"Error analyzing budget data: {str(e)}"
|
113 |
+
|
114 |
+
elif data_type == "portfolio":
|
115 |
+
try:
|
116 |
+
portfolio_data = json.loads(data)
|
117 |
+
holdings = portfolio_data.get("holdings", [])
|
118 |
+
total_value = sum(holding.get("value", 0) for holding in holdings)
|
119 |
+
|
120 |
+
analysis_text = "π **Advanced Portfolio Analysis**\n\n"
|
121 |
+
|
122 |
+
# Portfolio Overview
|
123 |
+
analysis_text += "## πΌ **Portfolio Overview**\n"
|
124 |
+
analysis_text += f"- **Total Portfolio Value**: ${total_value:,.2f}\n"
|
125 |
+
analysis_text += f"- **Number of Holdings**: {len(holdings)}\n"
|
126 |
+
|
127 |
+
if holdings:
|
128 |
+
values = [holding.get("value", 0) for holding in holdings]
|
129 |
+
avg_holding = sum(values) / len(values)
|
130 |
+
max_holding = max(values)
|
131 |
+
min_holding = min(values)
|
132 |
+
|
133 |
+
analysis_text += f"- **Average Holding Size**: ${avg_holding:,.2f}\n"
|
134 |
+
analysis_text += f"- **Largest Position**: ${max_holding:,.2f}\n"
|
135 |
+
analysis_text += f"- **Smallest Position**: ${min_holding:,.2f}\n"
|
136 |
+
|
137 |
+
# Detailed Holdings breakdown
|
138 |
+
analysis_text += "\n## π **Holdings Breakdown**\n"
|
139 |
+
sorted_holdings = sorted(holdings, key=lambda x: x.get("value", 0), reverse=True)
|
140 |
+
|
141 |
+
for i, holding in enumerate(sorted_holdings, 1):
|
142 |
+
symbol = holding.get("symbol", "Unknown")
|
143 |
+
value = holding.get("value", 0)
|
144 |
+
shares = holding.get("shares", 0)
|
145 |
+
allocation = holding.get("allocation", (value/total_value*100) if total_value > 0 else 0)
|
146 |
+
sector = holding.get("sector", "Unknown")
|
147 |
+
|
148 |
+
# Calculate position concentration risk
|
149 |
+
risk_level = "π’ Low" if allocation < 10 else "π‘ Medium" if allocation < 25 else "π΄ High"
|
150 |
+
|
151 |
+
analysis_text += f"**#{i} {symbol}** - {sector}\n"
|
152 |
+
analysis_text += f" ββ Value: ${value:,.2f} | Shares: {shares:,.0f} | Weight: {allocation:.1f}%\n"
|
153 |
+
analysis_text += f" ββ Concentration Risk: {risk_level}\n\n"
|
154 |
+
|
155 |
+
# Sector analysis with advanced metrics
|
156 |
+
sectors = {}
|
157 |
+
sector_values = {}
|
158 |
+
for holding in holdings:
|
159 |
+
sector = holding.get("sector", "Unknown")
|
160 |
+
allocation = holding.get("allocation", 0)
|
161 |
+
value = holding.get("value", 0)
|
162 |
+
|
163 |
+
sectors[sector] = sectors.get(sector, 0) + allocation
|
164 |
+
sector_values[sector] = sector_values.get(sector, 0) + value
|
165 |
+
|
166 |
+
if sectors:
|
167 |
+
analysis_text += "## π **Sector Diversification Analysis**\n"
|
168 |
+
sorted_sectors = sorted(sectors.items(), key=lambda x: x[1], reverse=True)
|
169 |
+
|
170 |
+
for sector, allocation in sorted_sectors:
|
171 |
+
bar = "β" * min(int(allocation / 2), 30)
|
172 |
+
value = sector_values.get(sector, 0)
|
173 |
+
|
174 |
+
# Sector concentration assessment
|
175 |
+
if allocation > 40:
|
176 |
+
risk_emoji = "π΄"
|
177 |
+
risk_text = "Over-concentrated"
|
178 |
+
elif allocation > 25:
|
179 |
+
risk_emoji = "π‘"
|
180 |
+
risk_text = "Moderate concentration"
|
181 |
+
else:
|
182 |
+
risk_emoji = "π’"
|
183 |
+
risk_text = "Well diversified"
|
184 |
+
|
185 |
+
analysis_text += f"**{sector}**: {allocation:.1f}% (${value:,.2f}) {risk_emoji}\n"
|
186 |
+
analysis_text += f" ββ {bar} {risk_text}\n\n"
|
187 |
+
|
188 |
+
# Portfolio Health Metrics
|
189 |
+
analysis_text += "## π― **Portfolio Health Assessment**\n"
|
190 |
+
|
191 |
+
# Diversification Score
|
192 |
+
num_sectors = len(sectors)
|
193 |
+
if num_sectors >= 8:
|
194 |
+
diversification = "π’ Excellent"
|
195 |
+
elif num_sectors >= 5:
|
196 |
+
diversification = "π‘ Good"
|
197 |
+
else:
|
198 |
+
diversification = "π΄ Poor"
|
199 |
+
|
200 |
+
analysis_text += f"- **Sector Diversification**: {diversification} ({num_sectors} sectors)\n"
|
201 |
+
|
202 |
+
# Concentration Risk
|
203 |
+
if holdings:
|
204 |
+
top_3_allocation = sum(sorted([h.get("allocation", 0) for h in holdings], reverse=True)[:3])
|
205 |
+
if top_3_allocation > 60:
|
206 |
+
concentration_risk = "π΄ High"
|
207 |
+
elif top_3_allocation > 40:
|
208 |
+
concentration_risk = "π‘ Medium"
|
209 |
+
else:
|
210 |
+
concentration_risk = "π’ Low"
|
211 |
+
|
212 |
+
analysis_text += f"- **Concentration Risk**: {concentration_risk} (Top 3: {top_3_allocation:.1f}%)\n"
|
213 |
+
|
214 |
+
# Portfolio Recommendations
|
215 |
+
analysis_text += "\n## π‘ **Portfolio Optimization Recommendations**\n"
|
216 |
+
|
217 |
+
# Check for over-concentration
|
218 |
+
for holding in holdings:
|
219 |
+
allocation = holding.get("allocation", 0)
|
220 |
+
if allocation > 25:
|
221 |
+
analysis_text += f"β οΈ **{holding.get('symbol', 'Unknown')}** is over-weighted at {allocation:.1f}% (consider rebalancing)\n"
|
222 |
+
|
223 |
+
# Sector recommendations
|
224 |
+
for sector, allocation in sectors.items():
|
225 |
+
if allocation > 40:
|
226 |
+
analysis_text += f"β οΈ **{sector}** sector over-weighted at {allocation:.1f}% (consider diversification)\n"
|
227 |
+
|
228 |
+
# Diversification suggestions
|
229 |
+
if num_sectors < 5:
|
230 |
+
analysis_text += "π‘ **Consider adding exposure to more sectors for better diversification**\n"
|
231 |
+
|
232 |
+
if len(holdings) < 10:
|
233 |
+
analysis_text += "π‘ **Consider adding more holdings to reduce single-stock risk**\n"
|
234 |
+
|
235 |
+
return analysis_text
|
236 |
+
except Exception as e:
|
237 |
+
return f"Error analyzing portfolio data: {str(e)}"
|
238 |
+
|
239 |
+
elif data_type == "stock":
|
240 |
+
try:
|
241 |
+
stock_data = json.loads(data)
|
242 |
+
symbol = stock_data.get("symbol", "Unknown")
|
243 |
+
price_str = stock_data.get("current_price", "0")
|
244 |
+
|
245 |
+
analysis_text = f"π **Comprehensive Stock Analysis: {symbol}**\n\n"
|
246 |
+
|
247 |
+
# Company Overview
|
248 |
+
analysis_text += "## π’ **Company Overview**\n"
|
249 |
+
analysis_text += f"- **Symbol**: {symbol}\n"
|
250 |
+
analysis_text += f"- **Current Price**: {price_str}\n"
|
251 |
+
analysis_text += f"- **Company**: {stock_data.get('company_name', 'N/A')}\n"
|
252 |
+
analysis_text += f"- **Sector**: {stock_data.get('sector', 'N/A')}\n"
|
253 |
+
analysis_text += f"- **Industry**: {stock_data.get('industry', 'N/A')}\n"
|
254 |
+
analysis_text += f"- **Market Cap**: {stock_data.get('market_cap', 'N/A')}\n\n"
|
255 |
+
|
256 |
+
# Financial Metrics
|
257 |
+
financials = stock_data.get("financials", {})
|
258 |
+
if financials:
|
259 |
+
analysis_text += "## πΉ **Key Financial Metrics**\n"
|
260 |
+
|
261 |
+
# Valuation metrics
|
262 |
+
pe_ratio = financials.get("pe_ratio", "N/A")
|
263 |
+
pb_ratio = financials.get("pb_ratio", "N/A")
|
264 |
+
ps_ratio = financials.get("ps_ratio", "N/A")
|
265 |
+
|
266 |
+
analysis_text += f"- **P/E Ratio**: {pe_ratio}"
|
267 |
+
if pe_ratio != "N/A" and isinstance(pe_ratio, (int, float)):
|
268 |
+
if pe_ratio < 15:
|
269 |
+
analysis_text += " π’ (Undervalued)"
|
270 |
+
elif pe_ratio > 25:
|
271 |
+
analysis_text += " π΄ (Potentially Overvalued)"
|
272 |
+
else:
|
273 |
+
analysis_text += " π‘ (Fairly Valued)"
|
274 |
+
analysis_text += "\n"
|
275 |
+
|
276 |
+
analysis_text += f"- **P/B Ratio**: {pb_ratio}\n"
|
277 |
+
analysis_text += f"- **P/S Ratio**: {ps_ratio}\n"
|
278 |
+
|
279 |
+
# Profitability metrics
|
280 |
+
analysis_text += f"- **ROE**: {financials.get('roe', 'N/A')}\n"
|
281 |
+
analysis_text += f"- **ROA**: {financials.get('roa', 'N/A')}\n"
|
282 |
+
analysis_text += f"- **Profit Margin**: {financials.get('profit_margin', 'N/A')}\n"
|
283 |
+
analysis_text += f"- **Revenue Growth**: {financials.get('revenue_growth', 'N/A')}\n\n"
|
284 |
+
|
285 |
+
# Performance analysis with trend indicators
|
286 |
+
performance = stock_data.get("performance", {})
|
287 |
+
if performance:
|
288 |
+
analysis_text += "## π **Performance Analysis**\n"
|
289 |
+
|
290 |
+
periods = ["1d", "1w", "1m", "3m", "6m", "1y", "ytd"]
|
291 |
+
for period in periods:
|
292 |
+
if period in performance:
|
293 |
+
return_pct = performance[period]
|
294 |
+
|
295 |
+
# Add trend indicators
|
296 |
+
if isinstance(return_pct, str) and "%" in return_pct:
|
297 |
+
try:
|
298 |
+
pct_value = float(return_pct.replace("%", ""))
|
299 |
+
if pct_value > 0:
|
300 |
+
trend = "π"
|
301 |
+
elif pct_value < 0:
|
302 |
+
trend = "π"
|
303 |
+
else:
|
304 |
+
trend = "β‘οΈ"
|
305 |
+
except:
|
306 |
+
trend = ""
|
307 |
+
else:
|
308 |
+
trend = ""
|
309 |
+
|
310 |
+
analysis_text += f"- **{period.upper()}**: {return_pct} {trend}\n"
|
311 |
+
analysis_text += "\n"
|
312 |
+
|
313 |
+
# Advanced Risk Assessment
|
314 |
+
risk_data = stock_data.get("risk_assessment", {})
|
315 |
+
if risk_data:
|
316 |
+
analysis_text += "## β οΈ **Risk Assessment**\n"
|
317 |
+
|
318 |
+
risk_level = risk_data.get('risk_level', 'N/A')
|
319 |
+
volatility = risk_data.get('volatility_30d', 'N/A')
|
320 |
+
beta = risk_data.get('beta', 'N/A')
|
321 |
+
|
322 |
+
# Risk level with emoji indicators
|
323 |
+
if risk_level.lower() == "low":
|
324 |
+
risk_emoji = "π’"
|
325 |
+
elif risk_level.lower() == "medium":
|
326 |
+
risk_emoji = "π‘"
|
327 |
+
elif risk_level.lower() == "high":
|
328 |
+
risk_emoji = "π΄"
|
329 |
+
else:
|
330 |
+
risk_emoji = ""
|
331 |
+
|
332 |
+
analysis_text += f"- **Risk Level**: {risk_level} {risk_emoji}\n"
|
333 |
+
analysis_text += f"- **30-Day Volatility**: {volatility}\n"
|
334 |
+
analysis_text += f"- **Beta**: {beta}"
|
335 |
+
|
336 |
+
if beta != "N/A" and isinstance(beta, (int, float)):
|
337 |
+
if beta > 1.2:
|
338 |
+
analysis_text += " (High volatility vs market)"
|
339 |
+
elif beta < 0.8:
|
340 |
+
analysis_text += " (Low volatility vs market)"
|
341 |
+
else:
|
342 |
+
analysis_text += " (Similar to market)"
|
343 |
+
analysis_text += "\n\n"
|
344 |
+
|
345 |
+
# Technical Analysis
|
346 |
+
technical = stock_data.get("technical_analysis", {})
|
347 |
+
if technical:
|
348 |
+
analysis_text += "## π **Technical Analysis**\n"
|
349 |
+
analysis_text += f"- **50-Day MA**: {technical.get('ma_50', 'N/A')}\n"
|
350 |
+
analysis_text += f"- **200-Day MA**: {technical.get('ma_200', 'N/A')}\n"
|
351 |
+
analysis_text += f"- **RSI**: {technical.get('rsi', 'N/A')}\n"
|
352 |
+
analysis_text += f"- **Support Level**: {technical.get('support', 'N/A')}\n"
|
353 |
+
analysis_text += f"- **Resistance Level**: {technical.get('resistance', 'N/A')}\n\n"
|
354 |
+
|
355 |
+
# Investment Recommendation with detailed reasoning
|
356 |
+
recommendation = stock_data.get("recommendation", {})
|
357 |
+
if recommendation:
|
358 |
+
action = recommendation.get('action', 'N/A')
|
359 |
+
confidence = recommendation.get('confidence', 'N/A')
|
360 |
+
reasoning = recommendation.get('reasoning', '')
|
361 |
+
|
362 |
+
analysis_text += "## π‘ **Investment Recommendation**\n"
|
363 |
+
|
364 |
+
# Action with emoji
|
365 |
+
if action.lower() == "buy":
|
366 |
+
action_emoji = "π’"
|
367 |
+
elif action.lower() == "sell":
|
368 |
+
action_emoji = "π΄"
|
369 |
+
elif action.lower() == "hold":
|
370 |
+
action_emoji = "π‘"
|
371 |
+
else:
|
372 |
+
action_emoji = ""
|
373 |
+
|
374 |
+
analysis_text += f"- **Action**: {action} {action_emoji}\n"
|
375 |
+
analysis_text += f"- **Confidence**: {confidence}\n"
|
376 |
+
|
377 |
+
if reasoning:
|
378 |
+
analysis_text += f"- **Reasoning**: {reasoning}\n"
|
379 |
+
|
380 |
+
analysis_text += "\n"
|
381 |
+
|
382 |
+
# Additional Investment Considerations
|
383 |
+
analysis_text += "## π― **Investment Considerations**\n"
|
384 |
+
|
385 |
+
# Dividend info
|
386 |
+
dividend_yield = stock_data.get("dividend_yield", "N/A")
|
387 |
+
if dividend_yield != "N/A":
|
388 |
+
analysis_text += f"- **Dividend Yield**: {dividend_yield}\n"
|
389 |
+
|
390 |
+
# Analyst ratings
|
391 |
+
analyst_rating = stock_data.get("analyst_rating", "N/A")
|
392 |
+
if analyst_rating != "N/A":
|
393 |
+
analysis_text += f"- **Analyst Rating**: {analyst_rating}\n"
|
394 |
+
|
395 |
+
# Price targets
|
396 |
+
price_target = stock_data.get("price_target", "N/A")
|
397 |
+
if price_target != "N/A":
|
398 |
+
analysis_text += f"- **Price Target**: {price_target}\n"
|
399 |
+
|
400 |
+
# ESG score
|
401 |
+
esg_score = stock_data.get("esg_score", "N/A")
|
402 |
+
if esg_score != "N/A":
|
403 |
+
analysis_text += f"- **ESG Score**: {esg_score}\n"
|
404 |
+
|
405 |
+
return analysis_text
|
406 |
+
except Exception as e:
|
407 |
+
return f"Error analyzing stock data: {str(e)}"
|
408 |
+
|
409 |
+
return None
|
410 |
+
|
411 |
+
|
412 |
+
def determine_intended_tool(message):
|
413 |
+
"""Determine which tool the AI intends to use based on the message"""
|
414 |
+
message_lower = message.lower()
|
415 |
+
|
416 |
+
tool_detection_map = {
|
417 |
+
"budget_planner": ["budget", "income", "expense", "spending", "allocat", "monthly", "plan", "financial plan", "money"],
|
418 |
+
"investment_analyzer": ["stock", "invest", "buy", "sell", "analyze", "AAPL", "GOOGL", "TSLA", "share", "equity"],
|
419 |
+
"portfolio_analyzer": ["portfolio", "holdings", "allocation", "diversif", "asset", "position"],
|
420 |
+
"market_trends": ["market", "trend", "news", "sector", "economic", "latest", "current"],
|
421 |
+
"expense_tracker": ["track", "expense", "spending", "categoriz", "cost"]
|
422 |
+
}
|
423 |
+
|
424 |
+
tool_names = {
|
425 |
+
"budget_planner": "Budget Planner",
|
426 |
+
"investment_analyzer": "Investment Analyzer",
|
427 |
+
"expense_tracker": "Expense Tracker",
|
428 |
+
"market_trends": "Market Trends Analyzer",
|
429 |
+
"portfolio_analyzer": "Portfolio Analyzer",
|
430 |
+
}
|
431 |
+
|
432 |
+
for tool_key, keywords in tool_detection_map.items():
|
433 |
+
if any(keyword in message_lower for keyword in keywords):
|
434 |
+
return tool_key, tool_names.get(tool_key, tool_key)
|
435 |
+
|
436 |
+
return None, None
|
437 |
+
|
438 |
+
|
439 |
+
def determine_response_type(message):
|
440 |
+
"""Determine if user wants detailed report or short response"""
|
441 |
+
message_lower = message.lower()
|
442 |
+
|
443 |
+
# Keywords indicating detailed response preference
|
444 |
+
detailed_keywords = [
|
445 |
+
"detailed", "detail", "comprehensive", "thorough", "in-depth", "full analysis",
|
446 |
+
"complete", "report", "breakdown", "explain", "elaborate", "deep dive",
|
447 |
+
"extensive", "detailed analysis", "full report", "comprehensive report"
|
448 |
+
]
|
449 |
+
|
450 |
+
# Keywords indicating short response preference
|
451 |
+
short_keywords = [
|
452 |
+
"quick", "brief", "short", "summary", "concise", "simple", "fast",
|
453 |
+
"just tell me", "quickly", "in short", "tldr", "bottom line"
|
454 |
+
]
|
455 |
+
|
456 |
+
# Check for detailed indicators first
|
457 |
+
if any(keyword in message_lower for keyword in detailed_keywords):
|
458 |
+
return "detailed"
|
459 |
+
|
460 |
+
# Check for short indicators
|
461 |
+
if any(keyword in message_lower for keyword in short_keywords):
|
462 |
+
return "short"
|
463 |
+
|
464 |
+
# Default to short response
|
465 |
+
return "short"
|
466 |
+
|
467 |
+
|
468 |
+
def process_financial_query(message, history):
|
469 |
+
"""Process user queries through the financial agent with streaming response"""
|
470 |
+
# Get the actual user message from the last entry in history
|
471 |
+
if not history or len(history) == 0:
|
472 |
+
return history
|
473 |
+
|
474 |
+
# Extract the last user message
|
475 |
+
last_user_message = None
|
476 |
+
for msg in reversed(history):
|
477 |
+
if msg["role"] == "user":
|
478 |
+
last_user_message = msg["content"]
|
479 |
+
break
|
480 |
+
|
481 |
+
if not last_user_message:
|
482 |
+
return history
|
483 |
+
|
484 |
+
# Convert Gradio history to agent format (excluding the last user message we just added)
|
485 |
+
agent_history = []
|
486 |
+
for i, msg in enumerate(history[:-1]): # Exclude the last message
|
487 |
+
agent_history.append(
|
488 |
+
{
|
489 |
+
"role": msg["role"],
|
490 |
+
"content": msg["content"]
|
491 |
+
if isinstance(msg["content"], str)
|
492 |
+
else str(msg["content"]),
|
493 |
+
}
|
494 |
+
)
|
495 |
+
|
496 |
+
# Start timer
|
497 |
+
start_time = time.time()
|
498 |
+
init_message_start_index = len(history)
|
499 |
+
|
500 |
+
try:
|
501 |
+
# Show what tool will be used and processing status
|
502 |
+
intended_tool_key, intended_tool_name = determine_intended_tool(last_user_message)
|
503 |
+
response_type = determine_response_type(last_user_message)
|
504 |
+
|
505 |
+
# Always show status for all tools with expected time estimates
|
506 |
+
if intended_tool_name:
|
507 |
+
if intended_tool_key == "market_trends":
|
508 |
+
status_msg = f"π Fetching market news & analyzing trends (estimated 20-30 seconds)..."
|
509 |
+
elif intended_tool_key == "investment_analyzer":
|
510 |
+
status_msg = f"π Analyzing stock data & calculating metrics (estimated 10-15 seconds)..."
|
511 |
+
elif intended_tool_key == "budget_planner":
|
512 |
+
status_msg = f"π° Processing budget analysis (estimated 5-10 seconds)..."
|
513 |
+
elif intended_tool_key == "portfolio_analyzer":
|
514 |
+
status_msg = f"π Analyzing portfolio data (estimated 8-12 seconds)..."
|
515 |
+
elif intended_tool_key == "expense_tracker":
|
516 |
+
status_msg = f"π³ Processing expense analysis (estimated 5-10 seconds)..."
|
517 |
+
else:
|
518 |
+
status_msg = f"π Using {intended_tool_name} (estimated 5-15 seconds)..."
|
519 |
+
|
520 |
+
history.append(ChatMessage(role="assistant", content=status_msg))
|
521 |
+
yield history
|
522 |
+
else:
|
523 |
+
# If no tool detected, show generic processing message
|
524 |
+
history.append(ChatMessage(role="assistant", content=f"π§ Processing your request (estimated 10-15 seconds)..."))
|
525 |
+
yield history
|
526 |
+
|
527 |
+
# Process message through agent
|
528 |
+
response, tool_used, tool_result, all_tools, all_results = agent.process_message_with_details(
|
529 |
+
last_user_message, agent_history
|
530 |
+
)
|
531 |
+
|
532 |
+
# Clear the processing message now that tool is complete
|
533 |
+
if len(history) > init_message_start_index:
|
534 |
+
history.pop() # Remove the processing message
|
535 |
+
# Step 5: Show tool execution results
|
536 |
+
if all_tools and all_results:
|
537 |
+
# Remove initialization messages but keep all previous conversation and tool info
|
538 |
+
history = history[:init_message_start_index]
|
539 |
+
|
540 |
+
tool_names = {
|
541 |
+
"budget_planner": "Budget Planner",
|
542 |
+
"investment_analyzer": "Investment Analyzer",
|
543 |
+
"expense_tracker": "Expense Tracker",
|
544 |
+
"market_trends": "Market Trends Analyzer",
|
545 |
+
"portfolio_analyzer": "Portfolio Analyzer",
|
546 |
+
}
|
547 |
+
|
548 |
+
tool_emojis = {
|
549 |
+
"Budget Planner": "π°",
|
550 |
+
"Investment Analyzer": "π",
|
551 |
+
"Expense Tracker": "π³",
|
552 |
+
"Market Trends Analyzer": "π°",
|
553 |
+
"Portfolio Analyzer": "π",
|
554 |
+
}
|
555 |
+
|
556 |
+
# Show results for all tools used
|
557 |
+
for i, (used_tool, result) in enumerate(zip(all_tools, all_results)):
|
558 |
+
tool_display_name = tool_names.get(used_tool, used_tool)
|
559 |
+
|
560 |
+
if result:
|
561 |
+
# Format tool result for display
|
562 |
+
try:
|
563 |
+
import json
|
564 |
+
|
565 |
+
if result.startswith("{") or result.startswith("["):
|
566 |
+
# Pretty format JSON output
|
567 |
+
parsed_result = json.loads(result)
|
568 |
+
# Truncate very long results for display
|
569 |
+
if len(str(parsed_result)) > 2000:
|
570 |
+
# Show summary for long results
|
571 |
+
if isinstance(parsed_result, dict):
|
572 |
+
summary = {
|
573 |
+
k: f"[{type(v).__name__}]"
|
574 |
+
if isinstance(v, (list, dict))
|
575 |
+
else v
|
576 |
+
for k, v in list(parsed_result.items())[:10]
|
577 |
+
}
|
578 |
+
display_result = f"```json\n{json.dumps(summary, indent=2)}\n... (truncated)\n```"
|
579 |
+
else:
|
580 |
+
display_result = f"```json\n{json.dumps(parsed_result, indent=2)[:1000]}...\n```"
|
581 |
+
else:
|
582 |
+
formatted_result = json.dumps(parsed_result, indent=2)
|
583 |
+
display_result = f"```json\n{formatted_result}\n```"
|
584 |
+
else:
|
585 |
+
# Truncate non-JSON results
|
586 |
+
display_result = (
|
587 |
+
result[:1000] + "..."
|
588 |
+
if len(result) > 1000
|
589 |
+
else result
|
590 |
+
)
|
591 |
+
except Exception as e:
|
592 |
+
display_result = (
|
593 |
+
str(result)[:1000] + "..."
|
594 |
+
if len(str(result)) > 1000
|
595 |
+
else str(result)
|
596 |
+
)
|
597 |
+
|
598 |
+
tool_emoji = tool_emojis.get(tool_display_name, "π§")
|
599 |
+
|
600 |
+
collapsible_content = f"""
|
601 |
+
<details>
|
602 |
+
<summary><strong>{tool_emoji} {tool_display_name} Results</strong> - Click to expand</summary>
|
603 |
+
|
604 |
+
{display_result}
|
605 |
+
|
606 |
+
</details>
|
607 |
+
"""
|
608 |
+
|
609 |
+
history.append(ChatMessage(
|
610 |
+
role="assistant",
|
611 |
+
content=collapsible_content,
|
612 |
+
))
|
613 |
+
yield history
|
614 |
+
|
615 |
+
# Add visualization for all applicable tools
|
616 |
+
if all_tools and all_results:
|
617 |
+
for used_tool, result in zip(all_tools, all_results):
|
618 |
+
if result and used_tool in ["budget_planner", "portfolio_analyzer", "investment_analyzer"]:
|
619 |
+
viz_type = {
|
620 |
+
"budget_planner": "budget",
|
621 |
+
"portfolio_analyzer": "portfolio",
|
622 |
+
"investment_analyzer": "stock",
|
623 |
+
}.get(used_tool)
|
624 |
+
|
625 |
+
try:
|
626 |
+
analysis_data = analyze_data_with_repl(viz_type, result)
|
627 |
+
|
628 |
+
if analysis_data:
|
629 |
+
tool_display_name = {
|
630 |
+
"budget_planner": "Budget",
|
631 |
+
"portfolio_analyzer": "Portfolio",
|
632 |
+
"investment_analyzer": "Stock",
|
633 |
+
}.get(used_tool, "Data")
|
634 |
+
|
635 |
+
# Create collapsible data analysis output
|
636 |
+
collapsible_analysis = f"""
|
637 |
+
<details>
|
638 |
+
<summary><strong>π {tool_display_name} Data Analysis</strong> - Click to expand</summary>
|
639 |
+
|
640 |
+
{analysis_data}
|
641 |
+
|
642 |
+
</details>
|
643 |
+
"""
|
644 |
+
|
645 |
+
history.append(ChatMessage(
|
646 |
+
role="assistant",
|
647 |
+
content=collapsible_analysis,
|
648 |
+
))
|
649 |
+
yield history
|
650 |
+
|
651 |
+
except Exception as e:
|
652 |
+
# Silently continue if analysis fails
|
653 |
+
pass
|
654 |
+
|
655 |
+
# Stream the final response in real-time using LLM streaming
|
656 |
+
if tool_used and tool_result:
|
657 |
+
# Use real LLM streaming with response type
|
658 |
+
streaming_content = ""
|
659 |
+
history.append(ChatMessage(role="assistant", content=""))
|
660 |
+
|
661 |
+
for chunk in agent.stream_response(last_user_message, tool_result, tool_used, response_type):
|
662 |
+
streaming_content += chunk
|
663 |
+
history[-1] = ChatMessage(role="assistant", content=streaming_content)
|
664 |
+
yield history
|
665 |
+
else:
|
666 |
+
# Fallback for non-streaming response
|
667 |
+
history.append(ChatMessage(role="assistant", content=response))
|
668 |
+
yield history
|
669 |
+
|
670 |
+
elapsed = time.time() - start_time
|
671 |
+
|
672 |
+
except Exception as e:
|
673 |
+
elapsed = time.time() - start_time
|
674 |
+
history[-1] = ChatMessage(
|
675 |
+
role="assistant",
|
676 |
+
content=f"I encountered an error while processing your request: {str(e)}. Please try rephrasing your question.",
|
677 |
+
metadata={"title": f"π₯ Error ({elapsed:.1f}s)"},
|
678 |
+
)
|
679 |
+
yield history
|
680 |
+
|
681 |
+
|
682 |
+
# Create the Gradio interface
|
683 |
+
with gr.Blocks(theme=gr.themes.Base(), title="Financial Advisory Agent") as demo:
|
684 |
+
gr.HTML("""<center><img src="/gradio_api/file=public/images/fin_logo.png" alt="Fin Logo" style="width: 50px; vertical-align: middle;">
|
685 |
+
<h1 style="text-align: center;">AI Financial Advisory Agent</h1>
|
686 |
+
Your AI-powered financial advisor for budgeting, investments, expense tracking, portfolio analysis, and market trends.
|
687 |
+
</center>
|
688 |
+
""")
|
689 |
+
|
690 |
+
chatbot = gr.Chatbot(
|
691 |
+
type="messages",
|
692 |
+
scale=2,
|
693 |
+
height=400,
|
694 |
+
avatar_images=AVATAR_IMAGES,
|
695 |
+
show_copy_button=True,
|
696 |
+
)
|
697 |
+
|
698 |
+
with gr.Row(equal_height=True):
|
699 |
+
msg = gr.Textbox(
|
700 |
+
placeholder="Ask me about budgeting, investments, or any financial topic...",
|
701 |
+
show_label=False,
|
702 |
+
scale=19,
|
703 |
+
autofocus=True,
|
704 |
+
)
|
705 |
+
submit = gr.Button("Send", variant="primary", scale=1, min_width=60)
|
706 |
+
|
707 |
+
# Example queries
|
708 |
+
example_queries = [
|
709 |
+
"Analyze AAPL stock and tell me if it's a good investment",
|
710 |
+
"Help me create a budget with $5000 monthly income and expenses: rent $1500, food $500, utilities $200",
|
711 |
+
"What are the latest market trends in tech stocks?",
|
712 |
+
"Analyze my portfolio: {'holdings': [{'symbol': 'AAPL', 'shares': 100}, {'symbol': 'GOOGL', 'shares': 50}]}",
|
713 |
+
]
|
714 |
+
|
715 |
+
gr.Examples(examples=example_queries, inputs=msg, label="Example Queries")
|
716 |
+
|
717 |
+
# Handle message submission
|
718 |
+
def user_submit(message, history):
|
719 |
+
if not message.strip():
|
720 |
+
return "", history, gr.update(interactive=True), gr.update(interactive=True)
|
721 |
+
history = history + [ChatMessage(role="user", content=message)]
|
722 |
+
return "", history, gr.update(interactive=False), gr.update(interactive=False)
|
723 |
+
|
724 |
+
def enable_input():
|
725 |
+
return gr.update(interactive=True), gr.update(interactive=True)
|
726 |
+
|
727 |
+
# Connect events
|
728 |
+
submit_event = (
|
729 |
+
msg.submit(user_submit, [msg, chatbot], [msg, chatbot, msg, submit])
|
730 |
+
.then(process_financial_query, [msg, chatbot], chatbot)
|
731 |
+
.then(enable_input, [], [msg, submit])
|
732 |
+
)
|
733 |
+
|
734 |
+
click_event = (
|
735 |
+
submit.click(user_submit, [msg, chatbot], [msg, chatbot, msg, submit])
|
736 |
+
.then(process_financial_query, [msg, chatbot], chatbot)
|
737 |
+
.then(enable_input, [], [msg, submit])
|
738 |
+
)
|
739 |
+
|
740 |
+
# Add like functionality for feedback
|
741 |
+
def like_handler(evt: gr.LikeData):
|
742 |
+
pass
|
743 |
+
|
744 |
+
chatbot.like(like_handler)
|
745 |
+
|
746 |
+
if __name__ == "__main__":
|
747 |
+
demo.launch(debug=True)
|
public/images/fin_logo.png
ADDED
![]() |
requirements.txt
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
gradio==5.33.0
|
2 |
+
langgraph==0.4.8
|
3 |
+
langchain==0.3.25
|
4 |
+
langchain-openai==0.3.21
|
5 |
+
langchain-community==0.3.24
|
6 |
+
tavily-python==0.7.5
|
7 |
+
yfinance==0.2.62
|
8 |
+
pandas==2.3.0
|
9 |
+
numpy==2.3.0
|
10 |
+
requests==2.32.3
|