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 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: red
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
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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