Asish Karthikeya Gogineni commited on
Commit
3e30d53
·
0 Parent(s):

Deploy Sentinel AI from GitHub

Browse files
.gitattributes ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # .gitattributes for Hugging Face Spaces
2
+ # This file tells git to use LFS for binary files
3
+
4
+ *.png filter=lfs diff=lfs merge=lfs -text
5
+ *.jpg filter=lfs diff=lfs merge=lfs -text
6
+ *.jpeg filter=lfs diff=lfs merge=lfs -text
7
+ *.gif filter=lfs diff=lfs merge=lfs -text
8
+ *.pdf filter=lfs diff=lfs merge=lfs -text
9
+ # Additional binary formats from HF defaults
10
+ *.7z filter=lfs diff=lfs merge=lfs -text
11
+ *.arrow filter=lfs diff=lfs merge=lfs -text
12
+ *.bin filter=lfs diff=lfs merge=lfs -text
13
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
14
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
15
+ *.h5 filter=lfs diff=lfs merge=lfs -text
16
+ *.model filter=lfs diff=lfs merge=lfs -text
17
+ *.onnx filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pkl filter=lfs diff=lfs merge=lfs -text
20
+ *.pt filter=lfs diff=lfs merge=lfs -text
21
+ *.pth filter=lfs diff=lfs merge=lfs -text
22
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
23
+ *.tar filter=lfs diff=lfs merge=lfs -text
24
+ *.zip filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ venv/
6
+ .env
7
+ .DS_Store
8
+
9
+ # Logs
10
+ logs/
11
+ *.log
12
+
13
+ # Database
14
+ *.db
15
+
16
+ # IDE
17
+ .vscode/
18
+ .idea/
.streamlit/config.toml ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ [theme]
2
+ base="dark"
3
+ primaryColor="#3b82f6"
4
+ backgroundColor="#0b0e11"
5
+ secondaryBackgroundColor="#15191e"
6
+ textColor="#e2e8f0"
7
+ font="sans serif"
.streamlit/secrets.toml.example ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ # Streamlit secrets (TOML format)
2
+ # Add your API keys here
3
+
4
+ TAVILY_API_KEY = "your-tavily-api-key-here"
5
+ ALPHA_VANTAGE_API_KEY = "your-alpha-vantage-api-key-here"
6
+ GOOGLE_API_KEY = "your-google-api-key-here"
7
+ GROQ_API_KEY = "your-groq-api-key-here"
Dockerfile ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.9-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Install system dependencies (if any)
6
+ RUN apt-get update && apt-get install -y \
7
+ build-essential \
8
+ curl \
9
+ software-properties-common \
10
+ git \
11
+ && rm -rf /var/lib/apt/lists/*
12
+
13
+ # Copy requirements first to leverage cache
14
+ COPY requirements.txt .
15
+ RUN pip install --no-cache-dir -r requirements.txt
16
+
17
+ # Copy the entire application
18
+ COPY . .
19
+
20
+ # Expose the port Hugging Face expects
21
+ EXPOSE 7860
22
+
23
+ # Run the orchestration script
24
+ CMD ["python", "main.py"]
README.md ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Sentinel AI Financial Intelligence
3
+ emoji: 🛡️
4
+ colorFrom: gray
5
+ colorTo: indigo
6
+ sdk: streamlit
7
+ sdk_version: 1.29.0
8
+ app_file: app.py
9
+ pinned: false
10
+ license: mit
11
+ ---
12
+
13
+ # Sentinel AI - Financial Intelligence Platform
14
+
15
+ Transform raw market data into actionable business insights with the power of AI. Analyze stocks, news, and portfolios automatically using intelligent agents.
16
+
17
+ ## Features
18
+
19
+ - 🧠 **Intelligent Analysis**: AI automatically understands market structures and generates insights
20
+ - 📊 **Smart Visualizations**: Creates appropriate charts and graphs with interactive visualizations
21
+ - 🎯 **Actionable Recommendations**: Get specific, measurable recommendations based on data-driven insights
22
+ - 🚨 **Live Wire**: Real-time market alerts and trending information
23
+
24
+ ## Technology Stack
25
+
26
+ - **Frontend**: Streamlit
27
+ - **AI/ML**: Google Gemini, LangGraph
28
+ - **Data Sources**: Alpha Vantage, Tavily Search
29
+ - **Architecture**: Multi-agent system with orchestrated workflows
30
+
31
+ ## Configuration
32
+
33
+ Before running, you need to set up the following secrets in Hugging Face Spaces settings:
34
+
35
+ ```toml
36
+ GOOGLE_API_KEY = "your-google-api-key"
37
+ ALPHA_VANTAGE_API_KEY = "your-alpha-vantage-key"
38
+ TAVILY_API_KEY = "your-tavily-api-key"
39
+ ```
40
+
41
+ ## Local Development
42
+
43
+ ```bash
44
+ pip install -r requirements.txt
45
+ streamlit run app.py
46
+ ```
47
+
48
+ ## License
49
+
50
+ MIT License - See LICENSE file for details
agents/data_analysis_agent.py ADDED
@@ -0,0 +1,245 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import numpy as np
3
+ import plotly.express as px
4
+ import json
5
+ import logging
6
+ import re
7
+ from typing import TypedDict, Dict, Any, List
8
+
9
+ from langgraph.graph import StateGraph, END
10
+ from langchain_google_genai import ChatGoogleGenerativeAI
11
+
12
+ logging.basicConfig(level=logging.INFO)
13
+ logger = logging.getLogger(__name__)
14
+
15
+ class AnalysisState(TypedDict):
16
+ dataframe: pd.DataFrame
17
+ dataset_info: Dict[str, Any]
18
+ insights: str
19
+ visualizations: List[Dict[str, Any]]
20
+ charts: List[Any]
21
+
22
+ class DataAnalysisAgent:
23
+ def __init__(self, llm: ChatGoogleGenerativeAI):
24
+ self.llm = llm
25
+ self.workflow = self._create_workflow()
26
+
27
+ def _create_workflow(self):
28
+ """Creates the graph workflow for the data analysis sub-agent."""
29
+ workflow = StateGraph(AnalysisState)
30
+ workflow.add_node("data_profiler", self._profile_dataset)
31
+ # This new node will generate insights AND plan visualizations in one LLM call
32
+ workflow.add_node("insight_and_viz_planner", self._generate_insights_and_plan_visualizations)
33
+ workflow.add_node("chart_creator", self._create_charts)
34
+
35
+ workflow.add_edge("data_profiler", "insight_and_viz_planner")
36
+ workflow.add_edge("insight_and_viz_planner", "chart_creator")
37
+ workflow.add_edge("chart_creator", END)
38
+
39
+ workflow.set_entry_point("data_profiler")
40
+ return workflow.compile()
41
+
42
+ def _profile_dataset(self, state: AnalysisState):
43
+ """Profiles the dataset to understand its structure for the LLM."""
44
+ logger.info("--- 📊 (Sub-Agent) Profiling Data ---")
45
+ df_for_profiling = state["dataframe"].copy().reset_index()
46
+
47
+ profile = {
48
+ "shape": df_for_profiling.shape,
49
+ "columns": list(df_for_profiling.columns),
50
+ "dtypes": {col: str(dtype) for col, dtype in df_for_profiling.dtypes.to_dict().items()},
51
+ "numeric_columns": df_for_profiling.select_dtypes(include=[np.number]).columns.tolist(),
52
+ "datetime_columns": df_for_profiling.select_dtypes(include=['datetime64']).columns.tolist()
53
+ }
54
+ logger.info(" Data profile created.")
55
+ return {"dataset_info": profile}
56
+
57
+ def _generate_insights_and_plan_visualizations(self, state: AnalysisState):
58
+ """Generates key insights and plans visualizations in a single LLM call."""
59
+ logger.info("--- 🧠 (Sub-Agent) Generating Insights & Visualization Plan ---")
60
+ info = state["dataset_info"]
61
+ datetime_col = info.get("datetime_columns", [None])[0] or info.get("columns", ["index"])[0]
62
+
63
+ prompt = f"""
64
+ You are an expert financial data scientist. Based on the following data profile from a time-series stock dataset,
65
+ generate key insights and plan effective visualizations.
66
+
67
+ Data Profile: {json.dumps(info, indent=2)}
68
+
69
+ Instructions:
70
+ Your response MUST be ONLY a single valid JSON object. Do not include any other text or markdown.
71
+ The JSON object must have two keys: "insights" and "visualizations".
72
+ - "insights": A list of 3-5 concise, bullet-point style strings focusing on trends, correlations, and anomalies.
73
+ - "visualizations": A list of 3 JSON objects, each planning a chart.
74
+ - Plan a line chart for the 'close' price over time using the '{datetime_col}' column.
75
+ - Plan a histogram for the 'volume' column.
76
+ - Plan one other relevant chart (e.g., scatter plot, bar chart).
77
+
78
+ Example Response:
79
+ {{
80
+ "insights": [
81
+ "The closing price shows a significant upward trend over the period.",
82
+ "Trading volume spiked on dates corresponding to major news events.",
83
+ "There is a strong positive correlation between opening and closing prices."
84
+ ],
85
+ "visualizations": [
86
+ {{"type": "line", "columns": ["{datetime_col}", "close"], "title": "Closing Price Over Time"}},
87
+ {{"type": "histogram", "columns": ["volume"], "title": "Trading Volume Distribution"}},
88
+ {{"type": "scatter", "columns": ["open", "close"], "title": "Opening vs. Closing Price"}}
89
+ ]
90
+ }}
91
+ """
92
+ response_str = self.llm.invoke(prompt).content
93
+ logger.info(f" LLM raw output for insights & viz plan:\n{response_str}")
94
+
95
+ try:
96
+ json_match = re.search(r'\{.*\}', response_str, re.DOTALL)
97
+ if not json_match:
98
+ raise ValueError("No JSON object found in the LLM response.")
99
+
100
+ clean_json_str = json_match.group(0)
101
+ response_json = json.loads(clean_json_str)
102
+
103
+ insights_list = response_json.get("insights", [])
104
+ insights_str = "\n".join(f"* {insight}" for insight in insights_list)
105
+ viz_plan = response_json.get("visualizations", [])
106
+
107
+ logger.info(" Successfully parsed insights and viz plan.")
108
+ return {"insights": insights_str, "visualizations": viz_plan}
109
+
110
+ except (json.JSONDecodeError, ValueError) as e:
111
+ logger.error(f"Failed to parse insights and visualization plan from LLM. Error: {e}")
112
+ logger.info(" Using a default visualization plan as a fallback.")
113
+ default_plan = [
114
+ {"type": "line", "columns": [datetime_col, "close"], "title": "Closing Price Over Time (Default)"},
115
+ {"type": "histogram", "columns": ["volume"], "title": "Trading Volume (Default)"}
116
+ ]
117
+ return {"insights": "Analysis generated, but detailed insights could not be parsed.", "visualizations": default_plan}
118
+
119
+ def _create_charts(self, state: AnalysisState):
120
+ """Creates Plotly charts - HARDCODED for reliability."""
121
+ logger.info("--- 🎨 (Sub-Agent) Creating Charts ---")
122
+
123
+ # 1. Prepare DataFrame
124
+ df = state["dataframe"].copy()
125
+ if df.index.name in ['timestamp', 'date', 'datetime', 'index']:
126
+ df = df.reset_index()
127
+
128
+ # Normalize column names to lowercase
129
+ df.columns = [str(c).lower() for c in df.columns]
130
+
131
+ charts = []
132
+
133
+ # Find X-axis column (timestamp)
134
+ x_col = None
135
+ for candidate in ['timestamp', 'date', 'datetime', 'index']:
136
+ if candidate in df.columns:
137
+ x_col = candidate
138
+ break
139
+
140
+ if not x_col:
141
+ logger.warning(" No timestamp column found. Skipping charts.")
142
+ return {"charts": []}
143
+
144
+ # --- CHART 1: Price History (Line) ---
145
+ if 'close' in df.columns:
146
+ try:
147
+ logger.info(f" Generating Price Chart (x={x_col}, y=close)")
148
+ fig = px.line(df, x=x_col, y='close',
149
+ title="📈 Price History",
150
+ template="plotly_dark",
151
+ labels={'close': 'Price ($)', x_col: 'Time'})
152
+ fig.update_traces(line_color='#00ff41')
153
+ charts.append(fig)
154
+ except Exception as e:
155
+ logger.error(f" Failed to generate price chart: {e}")
156
+
157
+ # --- CHART 2: Volume (Bar) ---
158
+ if 'volume' in df.columns:
159
+ try:
160
+ logger.info(f" Generating Volume Chart (x={x_col}, y=volume)")
161
+ fig = px.bar(df, x=x_col, y='volume',
162
+ title="📊 Trading Volume",
163
+ template="plotly_dark",
164
+ labels={'volume': 'Volume', x_col: 'Time'})
165
+ fig.update_traces(marker_color='#ff6b35')
166
+ charts.append(fig)
167
+ except Exception as e:
168
+ logger.error(f" Failed to generate volume chart: {e}")
169
+
170
+ # --- CHART 3: Price vs Volume (Scatter) ---
171
+ if 'close' in df.columns and 'volume' in df.columns:
172
+ try:
173
+ logger.info(" Generating Price vs Volume Scatter Plot")
174
+ fig = px.scatter(df, x='volume', y='close',
175
+ title="🔍 Price vs Volume Correlation",
176
+ template="plotly_dark",
177
+ labels={'volume': 'Trading Volume', 'close': 'Price ($)'},
178
+ trendline="ols", # Add regression line
179
+ opacity=0.6)
180
+ fig.update_traces(marker=dict(size=8, color='#4ecdc4'))
181
+ charts.append(fig)
182
+ except Exception as e:
183
+ logger.error(f" Failed to generate scatter plot: {e}")
184
+
185
+ # --- CHART 4: Daily Returns Histogram ---
186
+ if 'close' in df.columns and len(df) > 1:
187
+ try:
188
+ logger.info(" Generating Daily Returns Histogram")
189
+ # Calculate returns
190
+ df['returns'] = df['close'].pct_change() * 100
191
+ df_returns = df.dropna(subset=['returns'])
192
+
193
+ if not df_returns.empty:
194
+ fig = px.histogram(df_returns, x='returns',
195
+ nbins=30,
196
+ title="📊 Daily Returns Distribution",
197
+ template="plotly_dark",
198
+ labels={'returns': 'Daily Return (%)'},
199
+ color_discrete_sequence=['#9b59b6'])
200
+ fig.add_vline(x=0, line_dash="dash", line_color="white",
201
+ annotation_text="Zero Return", annotation_position="top")
202
+ charts.append(fig)
203
+ except Exception as e:
204
+ logger.error(f" Failed to generate histogram: {e}")
205
+
206
+ # --- CHART 5: Box Plot (Price Distribution) ---
207
+ if 'close' in df.columns:
208
+ try:
209
+ logger.info(" Generating Box Plot")
210
+ fig = px.box(df, y='close',
211
+ title="📦 Price Distribution (Box Plot)",
212
+ template="plotly_dark",
213
+ labels={'close': 'Price ($)'},
214
+ color_discrete_sequence=['#a29bfe'])
215
+ charts.append(fig)
216
+ except Exception as e:
217
+ logger.error(f" Failed to generate box plot: {e}")
218
+
219
+ # --- CHART 6: Violin Plot (Volume Distribution) ---
220
+ if 'volume' in df.columns:
221
+ try:
222
+ logger.info(" Generating Violin Plot")
223
+ fig = px.violin(df, y='volume',
224
+ title="🎻 Volume Distribution (Violin Plot)",
225
+ template="plotly_dark",
226
+ labels={'volume': 'Trading Volume'},
227
+ color_discrete_sequence=['#74b9ff'],
228
+ box=True, # Show box plot inside violin
229
+ points='all') # Show all data points
230
+ charts.append(fig)
231
+ except Exception as e:
232
+ logger.error(f" Failed to generate violin plot: {e}")
233
+
234
+ logger.info(f" Successfully created {len(charts)} charts.")
235
+ return {"charts": charts}
236
+
237
+ def run_analysis(self, dataframe: pd.DataFrame):
238
+ """Runs the full analysis workflow on the given DataFrame."""
239
+ if dataframe.empty:
240
+ logger.warning("Input DataFrame is empty. Skipping analysis.")
241
+ return {"insights": "No data available for analysis.", "charts": []}
242
+ initial_state = {"dataframe": dataframe}
243
+ # The final state will now contain insights and charts after the workflow runs
244
+ final_state = self.workflow.invoke(initial_state)
245
+ return final_state
agents/orchestrator_v3.py ADDED
@@ -0,0 +1,396 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import pandas as pd
4
+ import ast
5
+ from dotenv import load_dotenv
6
+ from typing import TypedDict, List, Dict, Any
7
+
8
+ from langgraph.graph import StateGraph, END
9
+
10
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
11
+
12
+ from agents.tool_calling_agents import WebResearchAgent, MarketDataAgent, InternalPortfolioAgent
13
+ from agents.data_analysis_agent import DataAnalysisAgent
14
+
15
+ from langchain_google_genai import ChatGoogleGenerativeAI
16
+
17
+ # --- Configuration ---
18
+ load_dotenv()
19
+
20
+ # --- Initialize workers (Stateless) ---
21
+ web_agent = WebResearchAgent()
22
+ market_agent = MarketDataAgent()
23
+ portfolio_agent = InternalPortfolioAgent()
24
+
25
+ # --- Define the Enhanced State ---
26
+ class AgentState(TypedDict):
27
+ task: str
28
+ symbol: str
29
+ web_research_results: str
30
+ market_data_results: str
31
+ portfolio_data_results: str
32
+ scan_intent: str # "DOWNWARD", "UPWARD", "ALL", or None
33
+ # --- NEW FIELDS FOR ANALYSIS ---
34
+ analysis_dataframe: pd.DataFrame
35
+ analysis_results: Dict[str, Any]
36
+ final_report: str
37
+ # Debug fields
38
+ debug_market_data_raw: Any
39
+ debug_dataframe_head: Any
40
+ debug_analysis_results_full: Any
41
+
42
+ def get_orchestrator(llm_provider="gemini", api_key=None):
43
+ """
44
+ Factory function to create the orchestrator graph with a specific LLM.
45
+ """
46
+
47
+ # 1. Initialize LLM (Gemini Only)
48
+ if not api_key:
49
+ api_key = os.getenv("GOOGLE_API_KEY")
50
+ if not api_key:
51
+ raise ValueError("Google Gemini API Key is missing.")
52
+ llm = ChatGoogleGenerativeAI(model="gemini-flash-lite-latest", google_api_key=api_key, temperature=0, max_retries=5)
53
+
54
+ # 2. Initialize Data Analyzer with the chosen LLM
55
+ data_analyzer = DataAnalysisAgent(llm=llm)
56
+
57
+ # 3. Define Nodes (Closure captures 'llm' and 'data_analyzer')
58
+
59
+ # 3. Define Nodes (Closure captures 'llm' and 'data_analyzer')
60
+
61
+ def extract_symbol_step(state: AgentState):
62
+ print("--- 🔬 Symbol & Time Range Extraction ---")
63
+ prompt = f"""
64
+ Analyze the user's request: "{state['task']}"
65
+
66
+ Extract TWO things:
67
+ 1. Stock symbol or scan intent
68
+ 2. Time range (if mentioned)
69
+
70
+ RULES:
71
+ - If request mentions a SPECIFIC company → Extract symbol
72
+ - If request mentions time period → Extract time range
73
+ - ONLY set scan_intent for "top gainers", "losers", "scan market"
74
+
75
+ Response Format: JSON ONLY.
76
+ {{
77
+ "symbol": "TICKER" or null,
78
+ "scan_intent": "DOWNWARD" | "UPWARD" | "ALL" or null,
79
+ "time_range": "INTRADAY" | "1D" | "3D" | "1W" | "1M" | "3M" | "1Y" or null
80
+ }}
81
+
82
+ Time Range Examples:
83
+ - "today", "now", "current", "recent" → "INTRADAY"
84
+ - "yesterday", "1 day back" → "1D"
85
+ - "3 days back", "last 3 days" → "3D"
86
+ - "last week", "1 week", "7 days" → "1W"
87
+ - "last month", "1 month", "30 days" → "1M"
88
+ - "3 months", "quarter" → "3M"
89
+ - "1 year", "12 months" → "1Y"
90
+
91
+ Full Examples:
92
+ - "Analyze Tesla" → {{"symbol": "TSLA", "scan_intent": null, "time_range": null}}
93
+ - "3 days back stocks of Tesla" → {{"symbol": "TSLA", "scan_intent": null, "time_range": "3D"}}
94
+ - "Last week AAPL performance" → {{"symbol": "AAPL", "scan_intent": null, "time_range": "1W"}}
95
+ - "1 month trend for NVDA" → {{"symbol": "NVDA", "scan_intent": null, "time_range": "1M"}}
96
+ - "Recent analysis of Tesla" → {{"symbol": "TSLA", "scan_intent": null, "time_range": "INTRADAY"}}
97
+ - "Show me top gainers" → {{"symbol": null, "scan_intent": "UPWARD", "time_range": null}}
98
+
99
+ CRITICAL: Default to null for time_range if not explicitly mentioned!
100
+ """
101
+ raw_response = llm.invoke(prompt).content.strip()
102
+
103
+ symbol = None
104
+ scan_intent = None
105
+ time_range = None
106
+
107
+ try:
108
+ import json
109
+ import re
110
+ # Find JSON in response
111
+ json_match = re.search(r'\{.*\}', raw_response, re.DOTALL)
112
+ if json_match:
113
+ data = json.loads(json_match.group(0))
114
+ symbol = data.get("symbol")
115
+ scan_intent = data.get("scan_intent")
116
+ time_range = data.get("time_range")
117
+ else:
118
+ print(f" WARNING: No JSON found in extraction response: {raw_response}")
119
+ # Fallback to simple cleaning
120
+ clean_resp = raw_response.strip().upper()
121
+ if "SCAN" in clean_resp or "GAINERS" in clean_resp or "LOSERS" in clean_resp:
122
+ scan_intent = "ALL"
123
+ elif len(clean_resp) <= 5 and clean_resp.isalpha():
124
+ symbol = clean_resp
125
+ except Exception as e:
126
+ print(f" Error parsing symbol extraction: {e}")
127
+
128
+ if symbol: symbol = symbol.upper().replace("$", "")
129
+
130
+ # Default time_range to INTRADAY if null (for backward compatibility)
131
+ if time_range is None:
132
+ time_range = "INTRADAY"
133
+
134
+ print(f" Raw LLM Response: {raw_response}")
135
+ print(f" Extracted Symbol: {symbol}")
136
+ print(f" Scan Intent: {scan_intent}")
137
+ print(f" Time Range: {time_range}")
138
+
139
+ return {"symbol": symbol, "scan_intent": scan_intent, "time_range": time_range}
140
+
141
+ def web_research_step(state: AgentState):
142
+ print("--- 🔎 Web Research ---")
143
+ if state.get("scan_intent"):
144
+ return {"web_research_results": "Market Scan initiated. Web research skipped for individual stock."}
145
+ results = web_agent.research(queries=[state['task']])
146
+ return {"web_research_results": results}
147
+
148
+ def market_data_step(state: AgentState):
149
+ print("--- 📊 Market Data Retrieval ---")
150
+
151
+ # Handle scan intent
152
+ if state.get("scan_intent"):
153
+ print(f" Scan Intent Detected: {state['scan_intent']}")
154
+
155
+ # Load watchlist
156
+ import json
157
+ watchlist_path = "watchlist.json"
158
+ if not os.path.exists(watchlist_path):
159
+ return {"market_data_results": {"error": "Watchlist not found. Please add symbols to your watchlist."}}
160
+
161
+ with open(watchlist_path, 'r') as f:
162
+ watchlist = json.load(f)
163
+
164
+ scan_results = []
165
+ scan_intent = state['scan_intent']
166
+
167
+ for sym in watchlist:
168
+ # Get compact data for speed (always use INTRADAY for scans)
169
+ data = market_agent.get_market_data(symbol=sym, time_range="INTRADAY")
170
+ if isinstance(data, dict) and 'data' in data:
171
+ ts = data['data']
172
+ sorted_times = sorted(ts.keys())
173
+ if len(sorted_times) > 0:
174
+ latest_time = sorted_times[-1]
175
+ earliest_time = sorted_times[0]
176
+ latest_close = float(ts[latest_time]['4. close'])
177
+ earliest_open = float(ts[earliest_time]['1. open'])
178
+ pct_change = ((latest_close - earliest_open) / earliest_open) * 100
179
+
180
+ # Filter based on scan intent
181
+ if scan_intent == "UPWARD" and pct_change > 0:
182
+ scan_results.append({"symbol": sym, "price": latest_close, "change": pct_change})
183
+ elif scan_intent == "DOWNWARD" and pct_change < 0:
184
+ scan_results.append({"symbol": sym, "price": latest_close, "change": pct_change})
185
+ elif scan_intent == "ALL":
186
+ scan_results.append({"symbol": sym, "price": latest_close, "change": pct_change})
187
+
188
+ # Sort by change
189
+ scan_results.sort(key=lambda x: x['change'], reverse=True)
190
+ return {"market_data_results": {"scan_results": scan_results}}
191
+
192
+ # Single symbol analysis
193
+ if not state.get("symbol"):
194
+ return {"market_data_results": "Skipped."}
195
+
196
+ time_range = state.get("time_range", "INTRADAY")
197
+ print(f" Fetching market data for {state['symbol']} (time_range={time_range})")
198
+ results = market_agent.get_market_data(symbol=state["symbol"], time_range=time_range)
199
+ return {"market_data_results": results, "debug_market_data_raw": results}
200
+
201
+ def portfolio_data_step(state: AgentState):
202
+ print("--- 💼 Internal Portfolio Data ---")
203
+ if state.get("scan_intent"):
204
+ return {"portfolio_data_results": "Market Scan initiated. Portfolio context skipped."}
205
+
206
+ if not state.get("symbol"):
207
+ return {"portfolio_data_results": "Skipped: No symbol provided."}
208
+
209
+ results = portfolio_agent.query_portfolio(question=f"What is the current exposure to {state['symbol']}?")
210
+ return {"portfolio_data_results": results}
211
+
212
+ def transform_data_step(state: AgentState):
213
+ print("--- 🔀 Transforming Data for Analysis ---")
214
+ if state.get("scan_intent"):
215
+ return {"analysis_dataframe": pd.DataFrame()} # Skip transformation for scan
216
+
217
+ market_data = state.get("market_data_results")
218
+
219
+ if not isinstance(market_data, dict) or not market_data.get('data'):
220
+ print(" Skipping transformation: No valid market data received.")
221
+ return {"analysis_dataframe": pd.DataFrame()}
222
+
223
+ try:
224
+ time_series_data = market_data.get('data')
225
+ if not time_series_data:
226
+ raise ValueError("The 'data' key is empty.")
227
+
228
+ df = pd.DataFrame.from_dict(time_series_data, orient='index')
229
+ df.index = pd.to_datetime(df.index)
230
+ df.index.name = "timestamp"
231
+ df.rename(columns={
232
+ '1. open': 'open', '2. high': 'high', '3. low': 'low',
233
+ '4. close': 'close', '5. volume': 'volume'
234
+ }, inplace=True)
235
+ df = df.apply(pd.to_numeric).sort_index()
236
+
237
+ print(f" Successfully created DataFrame with shape {df.shape}")
238
+ return {"analysis_dataframe": df, "debug_dataframe_head": df.head().to_dict()}
239
+ except Exception as e:
240
+ print(f" CRITICAL ERROR during data transformation: {e}")
241
+ return {"analysis_dataframe": pd.DataFrame()}
242
+
243
+ def run_data_analysis_step(state: AgentState):
244
+ print("--- 🔬 Running Deep-Dive Data Analysis ---")
245
+ if state.get("scan_intent"):
246
+ return {"analysis_results": {}} # Skip analysis for scan
247
+
248
+ df = state.get("analysis_dataframe")
249
+ if df is not None and not df.empty:
250
+ analysis_results = data_analyzer.run_analysis(df)
251
+ return {"analysis_results": analysis_results, "debug_analysis_results_full": analysis_results}
252
+ else:
253
+ print(" Skipping analysis: No data to analyze.")
254
+ return {"analysis_results": {}}
255
+
256
+ def synthesize_report_step(state: AgentState):
257
+ print("--- 📝 Synthesizing Final Report ---")
258
+
259
+ # Helper to truncate text to avoid Rate Limits
260
+ def truncate(text, max_chars=3000):
261
+ s = str(text)
262
+ if len(s) > max_chars:
263
+ return s[:max_chars] + "... (truncated)"
264
+ return s
265
+
266
+ # Check for Scan Results
267
+ market_data_res = state.get("market_data_results", {})
268
+ if isinstance(market_data_res, dict) and "scan_results" in market_data_res:
269
+ scan_results = market_data_res["scan_results"]
270
+ # Truncate scan results if necessary (though usually small)
271
+ scan_results_str = truncate(scan_results, 4000)
272
+
273
+ report_prompt = f"""
274
+ You are a senior financial analyst. The user requested a market scan: "{state['task']}".
275
+
276
+ Scan Results (from Watchlist):
277
+ {scan_results_str}
278
+
279
+ Generate a "Market Scan Report".
280
+ 1. Summary: Briefly explain the criteria and the overall market status based on these results.
281
+ 2. Results Table: Create a markdown table with columns: Symbol | Price | % Change.
282
+ 3. Conclusion: Highlight the most significant movers.
283
+ """
284
+ final_report = llm.invoke(report_prompt).content
285
+ return {"final_report": final_report}
286
+
287
+ analysis_insights = state.get("analysis_results", {}).get("insights", "Not available.")
288
+
289
+ # Truncate inputs for the main report
290
+ web_data = truncate(state.get('web_research_results', 'Not available.'), 3000)
291
+ market_summary = truncate(state.get('market_data_results', 'Not available'), 2000)
292
+ portfolio_data = truncate(state.get('portfolio_data_results', 'Not available.'), 2000)
293
+
294
+ # Extract Data Source
295
+ market_data_raw = state.get("market_data_results", {})
296
+ data_source = "Unknown"
297
+ if isinstance(market_data_raw, dict):
298
+ meta = market_data_raw.get("meta_data", {})
299
+ if isinstance(meta, dict):
300
+ data_source = meta.get("Source", "Real API (Alpha Vantage)")
301
+
302
+ report_prompt = f"""
303
+ You are a senior financial analyst writing a comprehensive "Alpha Report".
304
+ Your task is to synthesize all available information into a structured, cited report.
305
+
306
+ Original User Task: {state['task']}
307
+ Target Symbol: {state.get('symbol', 'Unknown')}
308
+ Data Source: {data_source}
309
+ ---
310
+ Available Information:
311
+ - Web Intelligence: {web_data}
312
+ - Market Data Summary: {market_summary}
313
+ - Deep-Dive Data Analysis Insights: {analysis_insights}
314
+ - Internal Portfolio Context: {portfolio_data}
315
+ ---
316
+
317
+ CRITICAL INSTRUCTIONS:
318
+ 1. First, evaluate the "Available Information".
319
+ - If the Target Symbol is 'Unknown' OR if the Web Intelligence and Market Data contain no meaningful information:
320
+ You MUST respond with: "I am not sure about this company as I could not find sufficient data."
321
+ Do NOT generate the rest of the report.
322
+
323
+ 2. Otherwise, generate the "Alpha Report" with the following sections:
324
+
325
+ > [!NOTE]
326
+ > **Data Source**: {data_source}
327
+
328
+ ## 1. Executive Summary
329
+ A 2-3 sentence overview of the key findings and current situation.
330
+
331
+ ## 2. Internal Context
332
+ Detail the firm's current exposure:
333
+ - IF the firm has shares > 0: Present as a markdown table:
334
+ | Symbol | Shares | Avg Cost | Current Value |
335
+ |--------|--------|----------|---------------|
336
+ - IF the firm has 0 shares: State: "The firm has no current exposure to {state.get('symbol')}."
337
+
338
+ ## 3. Market Data
339
+ ALWAYS present as a markdown table:
340
+ | Metric | Value | Implication |
341
+ |--------|-------|-------------|
342
+ | Current Price | $XXX.XX | +/-X.X% vs. open |
343
+ | 5-Day Trend | Upward/Downward/Flat | Brief note |
344
+ | Volume | X.XXM | Above/Below average |
345
+
346
+ ## 4. Real-Time Intelligence
347
+ ### News
348
+ - **[Headline]** - [Brief summary] `[Source: URL]`
349
+ - **[Headline]** - [Brief summary] `[Source: URL]`
350
+
351
+ ### Filings (if any)
352
+ - **[Filing Type]** - [Brief description] `[Source: URL]`
353
+
354
+ ## 5. Sentiment Analysis
355
+ **Overall Sentiment:** Bullish / Bearish / Neutral
356
+
357
+ **Evidence:**
358
+ - [Specific fact from news/data supporting this sentiment]
359
+ - [Another supporting fact]
360
+
361
+ ## 6. Synthesis & Recommendations
362
+ Combine all information to provide actionable insights. Focus on:
363
+ - Key risks and opportunities
364
+ - Recommended actions (if any)
365
+ - Items to monitor
366
+
367
+ FORMATTING RULES:
368
+ - Use markdown headers (##, ###)
369
+ - Include URLs in backticks: `[Source: example.com]`
370
+ - Use tables for structured data
371
+ - Be concise but comprehensive
372
+ """
373
+ final_report = llm.invoke(report_prompt).content
374
+ return {"final_report": final_report}
375
+
376
+ # 4. Build the Graph
377
+ workflow = StateGraph(AgentState)
378
+
379
+ workflow.add_node("extract_symbol", extract_symbol_step)
380
+ workflow.add_node("web_researcher", web_research_step)
381
+ workflow.add_node("market_data_analyst", market_data_step)
382
+ workflow.add_node("portfolio_data_fetcher", portfolio_data_step)
383
+ workflow.add_node("transform_data", transform_data_step)
384
+ workflow.add_node("data_analyzer", run_data_analysis_step)
385
+ workflow.add_node("report_synthesizer", synthesize_report_step)
386
+
387
+ workflow.set_entry_point("extract_symbol")
388
+ workflow.add_edge("extract_symbol", "web_researcher")
389
+ workflow.add_edge("web_researcher", "market_data_analyst")
390
+ workflow.add_edge("market_data_analyst", "portfolio_data_fetcher")
391
+ workflow.add_edge("portfolio_data_fetcher", "transform_data")
392
+ workflow.add_edge("transform_data", "data_analyzer")
393
+ workflow.add_edge("data_analyzer", "report_synthesizer")
394
+ workflow.add_edge("report_synthesizer", END)
395
+
396
+ return workflow.compile()
agents/tool_calling_agents.py ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # agents/tool_calling_agents.py (Corrected with longer timeout)
2
+ import httpx
3
+ import logging
4
+
5
+ # --- Configuration ---
6
+ import os
7
+ MCP_GATEWAY_URL = os.getenv("MCP_GATEWAY_URL", "http://127.0.0.1:8000/route_agent_request")
8
+
9
+ # --- Logging Setup ---
10
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
11
+ logger = logging.getLogger("ToolCallingAgents")
12
+
13
+ class BaseAgent:
14
+ """A base class for agents that call tools via the MCP Gateway."""
15
+ def __init__(self):
16
+ # A reasonable default timeout for fast, external APIs
17
+ self.client = httpx.Client(timeout=30.0)
18
+
19
+ def call_mcp_gateway(self, target_service: str, payload: dict) -> dict:
20
+ """A standardized method to make a request to the MCP Gateway."""
21
+ request_body = { "target_service": target_service, "payload": payload }
22
+ try:
23
+ logger.info(f"Agent calling MCP Gateway for service '{target_service}' with payload: {payload}")
24
+ response = self.client.post(MCP_GATEWAY_URL, json=request_body)
25
+ response.raise_for_status()
26
+ logger.info(f"Received successful response from MCP Gateway for '{target_service}'.")
27
+ return response.json()
28
+ except httpx.HTTPStatusError as e:
29
+ logger.error(f"Error response {e.response.status_code} from MCP Gateway: {e.response.text}")
30
+ raise
31
+ except httpx.RequestError as e:
32
+ logger.error(f"Failed to connect to MCP Gateway at {MCP_GATEWAY_URL}: {e}")
33
+ raise
34
+
35
+ class WebResearchAgent(BaseAgent):
36
+ """An agent specialized in performing web research using Tavily."""
37
+ def research(self, queries: list[str], search_depth: str = "basic") -> dict:
38
+ payload = { "queries": queries, "search_depth": search_depth }
39
+ return self.call_mcp_gateway("tavily_research", payload)
40
+
41
+ class MarketDataAgent(BaseAgent):
42
+ """An agent specialized in fetching financial market data."""
43
+ def get_market_data(self, symbol: str, time_range: str = "INTRADAY") -> dict:
44
+ payload = { "symbol": symbol, "time_range": time_range }
45
+ return self.call_mcp_gateway("alpha_vantage_market_data", payload)
46
+
47
+ class InternalPortfolioAgent(BaseAgent):
48
+ """An agent specialized in securely querying the internal portfolio database."""
49
+
50
+ # --- THIS IS THE FIX ---
51
+ def __init__(self):
52
+ # Override the default client with one that has a longer timeout
53
+ # because local LLM calls can be slow.
54
+ super().__init__()
55
+ self.client = httpx.Client(timeout=180.0) # Give it 180 seconds
56
+
57
+ def query_portfolio(self, question: str) -> dict:
58
+ payload = { "question": question }
59
+ return self.call_mcp_gateway("internal_portfolio_data", payload)
60
+
61
+ # --- Example Usage (for testing this file directly) ---
62
+ if __name__ == '__main__':
63
+ print("--- Testing Agents ---")
64
+
65
+ # Make sure all your MCP servers and the gateway are running.
66
+
67
+ # 1. Test the Web Research Agent
68
+ print("\n[1] Testing Web Research Agent...")
69
+ try:
70
+ web_agent = WebResearchAgent()
71
+ research_results = web_agent.research(queries=["What is the current market sentiment on NVIDIA?"])
72
+ print("Web Research Result:", research_results['status'])
73
+ except Exception as e:
74
+ print("Web Research Agent failed:", e)
75
+
76
+ # 2. Test the Market Data Agent
77
+ print("\n[2] Testing Market Data Agent...")
78
+ try:
79
+ market_agent = MarketDataAgent()
80
+ market_results = market_agent.get_intraday_data(symbol="TSLA", interval="15min")
81
+ print("Market Data Result:", market_results['status'])
82
+ except Exception as e:
83
+ print("Market Data Agent failed:", e)
84
+
85
+ # 3. Test the Internal Portfolio Agent
86
+ print("\n[3] Testing Internal Portfolio Agent...")
87
+ try:
88
+ portfolio_agent = InternalPortfolioAgent()
89
+ portfolio_results = portfolio_agent.query_portfolio(question="How many shares of AAPL do we own?")
90
+ print("Portfolio Query Result:", portfolio_results['status'])
91
+ except Exception as e:
92
+ print("Internal Portfolio Agent failed:", e)
alerts.json ADDED
@@ -0,0 +1,1102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "timestamp": "2025-12-07T00:21:28.690106",
4
+ "type": "MARKET",
5
+ "symbol": "AMZN",
6
+ "message": "\ud83d\udcc8 UP ALERT: AMZN moved +1.06% to $162.71",
7
+ "details": {
8
+ "price": 162.71,
9
+ "change": 1.0558350413017932,
10
+ "timestamp": "2025-12-07 00:21:28"
11
+ }
12
+ },
13
+ {
14
+ "timestamp": "2025-12-07T00:21:28.423214",
15
+ "type": "MARKET",
16
+ "symbol": "MSFT",
17
+ "message": "\ud83d\udcc9 DOWN ALERT: MSFT moved -0.98% to $433.44",
18
+ "details": {
19
+ "price": 433.44,
20
+ "change": -0.980056199026802,
21
+ "timestamp": "2025-12-07 00:21:28"
22
+ }
23
+ },
24
+ {
25
+ "timestamp": "2025-12-07T00:21:27.968412",
26
+ "type": "MARKET",
27
+ "symbol": "NVDA",
28
+ "message": "\ud83d\udcc9 DOWN ALERT: NVDA moved -0.73% to $393.71",
29
+ "details": {
30
+ "price": 393.71,
31
+ "change": -0.7286938981341511,
32
+ "timestamp": "2025-12-07 00:21:27"
33
+ }
34
+ },
35
+ {
36
+ "timestamp": "2025-12-07T00:21:27.597625",
37
+ "type": "MARKET",
38
+ "symbol": "TSLA",
39
+ "message": "\ud83d\udcc8 UP ALERT: TSLA moved +2.10% to $255.73",
40
+ "details": {
41
+ "price": 255.73,
42
+ "change": 2.1041284037371164,
43
+ "timestamp": "2025-12-07 00:21:27"
44
+ }
45
+ },
46
+ {
47
+ "timestamp": "2025-12-07T00:21:27.288732",
48
+ "type": "MARKET",
49
+ "symbol": "AAPL",
50
+ "message": "\ud83d\udcc8 UP ALERT: AAPL moved +2.46% to $190.98",
51
+ "details": {
52
+ "price": 190.98,
53
+ "change": 2.4625784645099005,
54
+ "timestamp": "2025-12-07 00:21:27"
55
+ }
56
+ },
57
+ {
58
+ "timestamp": "2025-12-07T00:21:16.724221",
59
+ "type": "MARKET",
60
+ "symbol": "AMZN",
61
+ "message": "\ud83d\udcc8 UP ALERT: AMZN moved +1.06% to $162.71",
62
+ "details": {
63
+ "price": 162.71,
64
+ "change": 1.0558350413017932,
65
+ "timestamp": "2025-12-07 00:21:16"
66
+ }
67
+ },
68
+ {
69
+ "timestamp": "2025-12-07T00:21:16.459301",
70
+ "type": "MARKET",
71
+ "symbol": "MSFT",
72
+ "message": "\ud83d\udcc9 DOWN ALERT: MSFT moved -0.98% to $433.44",
73
+ "details": {
74
+ "price": 433.44,
75
+ "change": -0.980056199026802,
76
+ "timestamp": "2025-12-07 00:21:16"
77
+ }
78
+ },
79
+ {
80
+ "timestamp": "2025-12-07T00:21:16.216594",
81
+ "type": "MARKET",
82
+ "symbol": "NVDA",
83
+ "message": "\ud83d\udcc9 DOWN ALERT: NVDA moved -0.73% to $393.71",
84
+ "details": {
85
+ "price": 393.71,
86
+ "change": -0.7286938981341511,
87
+ "timestamp": "2025-12-07 00:21:16"
88
+ }
89
+ },
90
+ {
91
+ "timestamp": "2025-12-07T00:21:15.967887",
92
+ "type": "MARKET",
93
+ "symbol": "TSLA",
94
+ "message": "\ud83d\udcc8 UP ALERT: TSLA moved +2.10% to $255.73",
95
+ "details": {
96
+ "price": 255.73,
97
+ "change": 2.1041284037371164,
98
+ "timestamp": "2025-12-07 00:21:15"
99
+ }
100
+ },
101
+ {
102
+ "timestamp": "2025-12-07T00:21:15.708764",
103
+ "type": "MARKET",
104
+ "symbol": "AAPL",
105
+ "message": "\ud83d\udcc8 UP ALERT: AAPL moved +2.46% to $190.98",
106
+ "details": {
107
+ "price": 190.98,
108
+ "change": 2.4625784645099005,
109
+ "timestamp": "2025-12-07 00:21:15"
110
+ }
111
+ },
112
+ {
113
+ "timestamp": "2025-12-07T00:21:05.175017",
114
+ "type": "MARKET",
115
+ "symbol": "AMZN",
116
+ "message": "\ud83d\udcc8 UP ALERT: AMZN moved +1.06% to $162.71",
117
+ "details": {
118
+ "price": 162.71,
119
+ "change": 1.0558350413017932,
120
+ "timestamp": "2025-12-07 00:21:05"
121
+ }
122
+ },
123
+ {
124
+ "timestamp": "2025-12-07T00:21:04.914370",
125
+ "type": "MARKET",
126
+ "symbol": "MSFT",
127
+ "message": "\ud83d\udcc9 DOWN ALERT: MSFT moved -0.98% to $433.44",
128
+ "details": {
129
+ "price": 433.44,
130
+ "change": -0.980056199026802,
131
+ "timestamp": "2025-12-07 00:21:04"
132
+ }
133
+ },
134
+ {
135
+ "timestamp": "2025-12-07T00:21:04.664472",
136
+ "type": "MARKET",
137
+ "symbol": "NVDA",
138
+ "message": "\ud83d\udcc9 DOWN ALERT: NVDA moved -0.73% to $393.71",
139
+ "details": {
140
+ "price": 393.71,
141
+ "change": -0.7286938981341511,
142
+ "timestamp": "2025-12-07 00:21:04"
143
+ }
144
+ },
145
+ {
146
+ "timestamp": "2025-12-07T00:21:04.416758",
147
+ "type": "MARKET",
148
+ "symbol": "TSLA",
149
+ "message": "\ud83d\udcc8 UP ALERT: TSLA moved +2.10% to $255.73",
150
+ "details": {
151
+ "price": 255.73,
152
+ "change": 2.1041284037371164,
153
+ "timestamp": "2025-12-07 00:21:04"
154
+ }
155
+ },
156
+ {
157
+ "timestamp": "2025-12-07T00:21:04.139567",
158
+ "type": "MARKET",
159
+ "symbol": "AAPL",
160
+ "message": "\ud83d\udcc8 UP ALERT: AAPL moved +2.46% to $190.98",
161
+ "details": {
162
+ "price": 190.98,
163
+ "change": 2.4625784645099005,
164
+ "timestamp": "2025-12-07 00:21:04"
165
+ }
166
+ },
167
+ {
168
+ "timestamp": "2025-12-07T00:20:53.847628",
169
+ "type": "MARKET",
170
+ "symbol": "GOOGL",
171
+ "message": "\ud83d\udcc9 DOWN ALERT: GOOGL moved -0.81% to $171.36",
172
+ "details": {
173
+ "price": 171.36,
174
+ "change": -0.8103727714748652,
175
+ "timestamp": "2025-12-07 00:20:53"
176
+ }
177
+ },
178
+ {
179
+ "timestamp": "2025-12-07T00:20:53.592544",
180
+ "type": "MARKET",
181
+ "symbol": "AMZN",
182
+ "message": "\ud83d\udcc9 DOWN ALERT: AMZN moved -0.60% to $184.50",
183
+ "details": {
184
+ "price": 184.5,
185
+ "change": -0.6033832561146453,
186
+ "timestamp": "2025-12-07 00:20:53"
187
+ }
188
+ },
189
+ {
190
+ "timestamp": "2025-12-07T00:20:53.337153",
191
+ "type": "MARKET",
192
+ "symbol": "MSFT",
193
+ "message": "\ud83d\udcc8 UP ALERT: MSFT moved +1.82% to $425.31",
194
+ "details": {
195
+ "price": 425.31,
196
+ "change": 1.8170066072967477,
197
+ "timestamp": "2025-12-07 00:20:53"
198
+ }
199
+ },
200
+ {
201
+ "timestamp": "2025-12-07T00:20:53.050481",
202
+ "type": "MARKET",
203
+ "symbol": "NVDA",
204
+ "message": "\ud83d\udcc8 UP ALERT: NVDA moved +1.78% to $488.34",
205
+ "details": {
206
+ "price": 488.34,
207
+ "change": 1.782029637966601,
208
+ "timestamp": "2025-12-07 00:20:53"
209
+ }
210
+ },
211
+ {
212
+ "timestamp": "2025-12-07T00:20:52.449885",
213
+ "type": "MARKET",
214
+ "symbol": "AAPL",
215
+ "message": "\ud83d\udcc8 UP ALERT: AAPL moved +2.03% to $136.77",
216
+ "details": {
217
+ "price": 136.77,
218
+ "change": 2.029093621782916,
219
+ "timestamp": "2025-12-07 00:20:52"
220
+ }
221
+ },
222
+ {
223
+ "timestamp": "2025-12-07T00:20:42.155953",
224
+ "type": "MARKET",
225
+ "symbol": "GOOGL",
226
+ "message": "\ud83d\udcc9 DOWN ALERT: GOOGL moved -0.81% to $171.36",
227
+ "details": {
228
+ "price": 171.36,
229
+ "change": -0.8103727714748652,
230
+ "timestamp": "2025-12-07 00:20:42"
231
+ }
232
+ },
233
+ {
234
+ "timestamp": "2025-12-07T00:20:41.898578",
235
+ "type": "MARKET",
236
+ "symbol": "AMZN",
237
+ "message": "\ud83d\udcc9 DOWN ALERT: AMZN moved -0.60% to $184.50",
238
+ "details": {
239
+ "price": 184.5,
240
+ "change": -0.6033832561146453,
241
+ "timestamp": "2025-12-07 00:20:41"
242
+ }
243
+ },
244
+ {
245
+ "timestamp": "2025-12-07T00:20:41.604483",
246
+ "type": "MARKET",
247
+ "symbol": "MSFT",
248
+ "message": "\ud83d\udcc8 UP ALERT: MSFT moved +1.82% to $425.31",
249
+ "details": {
250
+ "price": 425.31,
251
+ "change": 1.8170066072967477,
252
+ "timestamp": "2025-12-07 00:20:41"
253
+ }
254
+ },
255
+ {
256
+ "timestamp": "2025-12-07T00:20:41.354291",
257
+ "type": "MARKET",
258
+ "symbol": "NVDA",
259
+ "message": "\ud83d\udcc8 UP ALERT: NVDA moved +1.78% to $488.34",
260
+ "details": {
261
+ "price": 488.34,
262
+ "change": 1.782029637966601,
263
+ "timestamp": "2025-12-07 00:20:41"
264
+ }
265
+ },
266
+ {
267
+ "timestamp": "2025-12-07T00:20:40.851188",
268
+ "type": "MARKET",
269
+ "symbol": "AAPL",
270
+ "message": "\ud83d\udcc8 UP ALERT: AAPL moved +2.03% to $136.77",
271
+ "details": {
272
+ "price": 136.77,
273
+ "change": 2.029093621782916,
274
+ "timestamp": "2025-12-07 00:20:40"
275
+ }
276
+ },
277
+ {
278
+ "timestamp": "2025-12-07T00:20:30.572421",
279
+ "type": "MARKET",
280
+ "symbol": "GOOGL",
281
+ "message": "\ud83d\udcc9 DOWN ALERT: GOOGL moved -0.81% to $171.36",
282
+ "details": {
283
+ "price": 171.36,
284
+ "change": -0.8103727714748652,
285
+ "timestamp": "2025-12-07 00:20:30"
286
+ }
287
+ },
288
+ {
289
+ "timestamp": "2025-12-07T00:20:30.309398",
290
+ "type": "MARKET",
291
+ "symbol": "AMZN",
292
+ "message": "\ud83d\udcc9 DOWN ALERT: AMZN moved -0.60% to $184.50",
293
+ "details": {
294
+ "price": 184.5,
295
+ "change": -0.6033832561146453,
296
+ "timestamp": "2025-12-07 00:20:30"
297
+ }
298
+ },
299
+ {
300
+ "timestamp": "2025-12-07T00:20:30.035706",
301
+ "type": "MARKET",
302
+ "symbol": "MSFT",
303
+ "message": "\ud83d\udcc8 UP ALERT: MSFT moved +1.82% to $425.31",
304
+ "details": {
305
+ "price": 425.31,
306
+ "change": 1.8170066072967477,
307
+ "timestamp": "2025-12-07 00:20:30"
308
+ }
309
+ },
310
+ {
311
+ "timestamp": "2025-12-07T00:20:29.787691",
312
+ "type": "MARKET",
313
+ "symbol": "NVDA",
314
+ "message": "\ud83d\udcc8 UP ALERT: NVDA moved +1.78% to $488.34",
315
+ "details": {
316
+ "price": 488.34,
317
+ "change": 1.782029637966601,
318
+ "timestamp": "2025-12-07 00:20:29"
319
+ }
320
+ },
321
+ {
322
+ "timestamp": "2025-12-07T00:20:29.292529",
323
+ "type": "MARKET",
324
+ "symbol": "AAPL",
325
+ "message": "\ud83d\udcc8 UP ALERT: AAPL moved +2.03% to $136.77",
326
+ "details": {
327
+ "price": 136.77,
328
+ "change": 2.029093621782916,
329
+ "timestamp": "2025-12-07 00:20:29"
330
+ }
331
+ },
332
+ {
333
+ "timestamp": "2025-12-07T00:20:18.985643",
334
+ "type": "MARKET",
335
+ "symbol": "GOOGL",
336
+ "message": "\ud83d\udcc9 DOWN ALERT: GOOGL moved -0.81% to $171.36",
337
+ "details": {
338
+ "price": 171.36,
339
+ "change": -0.8103727714748652,
340
+ "timestamp": "2025-12-07 00:20:18"
341
+ }
342
+ },
343
+ {
344
+ "timestamp": "2025-12-07T00:20:18.706961",
345
+ "type": "MARKET",
346
+ "symbol": "AMZN",
347
+ "message": "\ud83d\udcc9 DOWN ALERT: AMZN moved -0.60% to $184.50",
348
+ "details": {
349
+ "price": 184.5,
350
+ "change": -0.6033832561146453,
351
+ "timestamp": "2025-12-07 00:20:18"
352
+ }
353
+ },
354
+ {
355
+ "timestamp": "2025-12-07T00:20:18.452874",
356
+ "type": "MARKET",
357
+ "symbol": "MSFT",
358
+ "message": "\ud83d\udcc8 UP ALERT: MSFT moved +1.82% to $425.31",
359
+ "details": {
360
+ "price": 425.31,
361
+ "change": 1.8170066072967477,
362
+ "timestamp": "2025-12-07 00:20:18"
363
+ }
364
+ },
365
+ {
366
+ "timestamp": "2025-12-07T00:20:18.179318",
367
+ "type": "MARKET",
368
+ "symbol": "NVDA",
369
+ "message": "\ud83d\udcc8 UP ALERT: NVDA moved +1.78% to $488.34",
370
+ "details": {
371
+ "price": 488.34,
372
+ "change": 1.782029637966601,
373
+ "timestamp": "2025-12-07 00:20:18"
374
+ }
375
+ },
376
+ {
377
+ "timestamp": "2025-12-07T00:20:17.316521",
378
+ "type": "MARKET",
379
+ "symbol": "AAPL",
380
+ "message": "\ud83d\udcc8 UP ALERT: AAPL moved +2.03% to $136.77",
381
+ "details": {
382
+ "price": 136.77,
383
+ "change": 2.029093621782916,
384
+ "timestamp": "2025-12-07 00:20:17"
385
+ }
386
+ },
387
+ {
388
+ "timestamp": "2025-12-07T00:20:07.062668",
389
+ "type": "MARKET",
390
+ "symbol": "GOOGL",
391
+ "message": "\ud83d\udcc9 DOWN ALERT: GOOGL moved -0.81% to $171.36",
392
+ "details": {
393
+ "price": 171.36,
394
+ "change": -0.8103727714748652,
395
+ "timestamp": "2025-12-07 00:20:07"
396
+ }
397
+ },
398
+ {
399
+ "timestamp": "2025-12-07T00:20:06.813052",
400
+ "type": "MARKET",
401
+ "symbol": "AMZN",
402
+ "message": "\ud83d\udcc9 DOWN ALERT: AMZN moved -0.60% to $184.50",
403
+ "details": {
404
+ "price": 184.5,
405
+ "change": -0.6033832561146453,
406
+ "timestamp": "2025-12-07 00:20:06"
407
+ }
408
+ },
409
+ {
410
+ "timestamp": "2025-12-07T00:20:06.564151",
411
+ "type": "MARKET",
412
+ "symbol": "MSFT",
413
+ "message": "\ud83d\udcc8 UP ALERT: MSFT moved +1.82% to $425.31",
414
+ "details": {
415
+ "price": 425.31,
416
+ "change": 1.8170066072967477,
417
+ "timestamp": "2025-12-07 00:20:06"
418
+ }
419
+ },
420
+ {
421
+ "timestamp": "2025-12-07T00:20:06.277580",
422
+ "type": "MARKET",
423
+ "symbol": "NVDA",
424
+ "message": "\ud83d\udcc8 UP ALERT: NVDA moved +1.78% to $488.34",
425
+ "details": {
426
+ "price": 488.34,
427
+ "change": 1.782029637966601,
428
+ "timestamp": "2025-12-07 00:20:06"
429
+ }
430
+ },
431
+ {
432
+ "timestamp": "2025-12-07T00:20:05.730732",
433
+ "type": "MARKET",
434
+ "symbol": "AAPL",
435
+ "message": "\ud83d\udcc8 UP ALERT: AAPL moved +2.03% to $136.77",
436
+ "details": {
437
+ "price": 136.77,
438
+ "change": 2.029093621782916,
439
+ "timestamp": "2025-12-07 00:20:05"
440
+ }
441
+ },
442
+ {
443
+ "timestamp": "2025-12-07T00:19:55.457222",
444
+ "type": "MARKET",
445
+ "symbol": "GOOGL",
446
+ "message": "\ud83d\udcc8 UP ALERT: GOOGL moved +2.33% to $161.03",
447
+ "details": {
448
+ "price": 161.03,
449
+ "change": 2.3257291732858847,
450
+ "timestamp": "2025-12-07 00:19:55"
451
+ }
452
+ },
453
+ {
454
+ "timestamp": "2025-12-07T00:19:54.959505",
455
+ "type": "MARKET",
456
+ "symbol": "MSFT",
457
+ "message": "\ud83d\udcc8 UP ALERT: MSFT moved +4.57% to $388.37",
458
+ "details": {
459
+ "price": 388.37,
460
+ "change": 4.574829016102108,
461
+ "timestamp": "2025-12-07 00:19:54"
462
+ }
463
+ },
464
+ {
465
+ "timestamp": "2025-12-07T00:19:54.706628",
466
+ "type": "MARKET",
467
+ "symbol": "NVDA",
468
+ "message": "\ud83d\udcc8 UP ALERT: NVDA moved +1.55% to $417.47",
469
+ "details": {
470
+ "price": 417.47,
471
+ "change": 1.5470312081924578,
472
+ "timestamp": "2025-12-07 00:19:54"
473
+ }
474
+ },
475
+ {
476
+ "timestamp": "2025-12-07T00:19:54.444375",
477
+ "type": "MARKET",
478
+ "symbol": "TSLA",
479
+ "message": "\ud83d\udcc9 DOWN ALERT: TSLA moved -0.67% to $232.45",
480
+ "details": {
481
+ "price": 232.45,
482
+ "change": -0.6666381778556483,
483
+ "timestamp": "2025-12-07 00:19:54"
484
+ }
485
+ },
486
+ {
487
+ "timestamp": "2025-12-07T00:19:54.175770",
488
+ "type": "MARKET",
489
+ "symbol": "AAPL",
490
+ "message": "\ud83d\udcc9 DOWN ALERT: AAPL moved -0.53% to $174.63",
491
+ "details": {
492
+ "price": 174.63,
493
+ "change": -0.5297334244702704,
494
+ "timestamp": "2025-12-07 00:19:54"
495
+ }
496
+ },
497
+ {
498
+ "timestamp": "2025-12-07T00:19:43.879355",
499
+ "type": "MARKET",
500
+ "symbol": "GOOGL",
501
+ "message": "\ud83d\udcc8 UP ALERT: GOOGL moved +2.33% to $161.03",
502
+ "details": {
503
+ "price": 161.03,
504
+ "change": 2.3257291732858847,
505
+ "timestamp": "2025-12-07 00:19:43"
506
+ }
507
+ },
508
+ {
509
+ "timestamp": "2025-12-07T00:19:43.353084",
510
+ "type": "MARKET",
511
+ "symbol": "MSFT",
512
+ "message": "\ud83d\udcc8 UP ALERT: MSFT moved +4.57% to $388.37",
513
+ "details": {
514
+ "price": 388.37,
515
+ "change": 4.574829016102108,
516
+ "timestamp": "2025-12-07 00:19:43"
517
+ }
518
+ },
519
+ {
520
+ "timestamp": "2025-12-07T00:19:43.108885",
521
+ "type": "MARKET",
522
+ "symbol": "NVDA",
523
+ "message": "\ud83d\udcc8 UP ALERT: NVDA moved +1.55% to $417.47",
524
+ "details": {
525
+ "price": 417.47,
526
+ "change": 1.5470312081924578,
527
+ "timestamp": "2025-12-07 00:19:43"
528
+ }
529
+ },
530
+ {
531
+ "timestamp": "2025-12-07T00:19:42.833784",
532
+ "type": "MARKET",
533
+ "symbol": "TSLA",
534
+ "message": "\ud83d\udcc9 DOWN ALERT: TSLA moved -0.67% to $232.45",
535
+ "details": {
536
+ "price": 232.45,
537
+ "change": -0.6666381778556483,
538
+ "timestamp": "2025-12-07 00:19:42"
539
+ }
540
+ },
541
+ {
542
+ "timestamp": "2025-12-07T00:19:42.562678",
543
+ "type": "MARKET",
544
+ "symbol": "AAPL",
545
+ "message": "\ud83d\udcc9 DOWN ALERT: AAPL moved -0.53% to $174.63",
546
+ "details": {
547
+ "price": 174.63,
548
+ "change": -0.5297334244702704,
549
+ "timestamp": "2025-12-07 00:19:42"
550
+ }
551
+ },
552
+ {
553
+ "timestamp": "2025-12-07T00:19:32.270914",
554
+ "type": "MARKET",
555
+ "symbol": "GOOGL",
556
+ "message": "\ud83d\udcc8 UP ALERT: GOOGL moved +2.33% to $161.03",
557
+ "details": {
558
+ "price": 161.03,
559
+ "change": 2.3257291732858847,
560
+ "timestamp": "2025-12-07 00:19:32"
561
+ }
562
+ },
563
+ {
564
+ "timestamp": "2025-12-07T00:19:31.741726",
565
+ "type": "MARKET",
566
+ "symbol": "MSFT",
567
+ "message": "\ud83d\udcc8 UP ALERT: MSFT moved +4.57% to $388.37",
568
+ "details": {
569
+ "price": 388.37,
570
+ "change": 4.574829016102108,
571
+ "timestamp": "2025-12-07 00:19:31"
572
+ }
573
+ },
574
+ {
575
+ "timestamp": "2025-12-07T00:19:31.484246",
576
+ "type": "MARKET",
577
+ "symbol": "NVDA",
578
+ "message": "\ud83d\udcc8 UP ALERT: NVDA moved +1.55% to $417.47",
579
+ "details": {
580
+ "price": 417.47,
581
+ "change": 1.5470312081924578,
582
+ "timestamp": "2025-12-07 00:19:31"
583
+ }
584
+ },
585
+ {
586
+ "timestamp": "2025-12-07T00:19:31.235216",
587
+ "type": "MARKET",
588
+ "symbol": "TSLA",
589
+ "message": "\ud83d\udcc9 DOWN ALERT: TSLA moved -0.67% to $232.45",
590
+ "details": {
591
+ "price": 232.45,
592
+ "change": -0.6666381778556483,
593
+ "timestamp": "2025-12-07 00:19:31"
594
+ }
595
+ },
596
+ {
597
+ "timestamp": "2025-12-07T00:19:30.943159",
598
+ "type": "MARKET",
599
+ "symbol": "AAPL",
600
+ "message": "\ud83d\udcc9 DOWN ALERT: AAPL moved -0.53% to $174.63",
601
+ "details": {
602
+ "price": 174.63,
603
+ "change": -0.5297334244702704,
604
+ "timestamp": "2025-12-07 00:19:30"
605
+ }
606
+ },
607
+ {
608
+ "timestamp": "2025-12-07T00:19:20.683026",
609
+ "type": "MARKET",
610
+ "symbol": "GOOGL",
611
+ "message": "\ud83d\udcc8 UP ALERT: GOOGL moved +2.33% to $161.03",
612
+ "details": {
613
+ "price": 161.03,
614
+ "change": 2.3257291732858847,
615
+ "timestamp": "2025-12-07 00:19:20"
616
+ }
617
+ },
618
+ {
619
+ "timestamp": "2025-12-07T00:19:20.171274",
620
+ "type": "MARKET",
621
+ "symbol": "MSFT",
622
+ "message": "\ud83d\udcc8 UP ALERT: MSFT moved +4.57% to $388.37",
623
+ "details": {
624
+ "price": 388.37,
625
+ "change": 4.574829016102108,
626
+ "timestamp": "2025-12-07 00:19:20"
627
+ }
628
+ },
629
+ {
630
+ "timestamp": "2025-12-07T00:19:19.925430",
631
+ "type": "MARKET",
632
+ "symbol": "NVDA",
633
+ "message": "\ud83d\udcc8 UP ALERT: NVDA moved +1.55% to $417.47",
634
+ "details": {
635
+ "price": 417.47,
636
+ "change": 1.5470312081924578,
637
+ "timestamp": "2025-12-07 00:19:19"
638
+ }
639
+ },
640
+ {
641
+ "timestamp": "2025-12-07T00:19:19.667863",
642
+ "type": "MARKET",
643
+ "symbol": "TSLA",
644
+ "message": "\ud83d\udcc9 DOWN ALERT: TSLA moved -0.67% to $232.45",
645
+ "details": {
646
+ "price": 232.45,
647
+ "change": -0.6666381778556483,
648
+ "timestamp": "2025-12-07 00:19:19"
649
+ }
650
+ },
651
+ {
652
+ "timestamp": "2025-12-07T00:19:19.367448",
653
+ "type": "MARKET",
654
+ "symbol": "AAPL",
655
+ "message": "\ud83d\udcc9 DOWN ALERT: AAPL moved -0.53% to $174.63",
656
+ "details": {
657
+ "price": 174.63,
658
+ "change": -0.5297334244702704,
659
+ "timestamp": "2025-12-07 00:19:19"
660
+ }
661
+ },
662
+ {
663
+ "timestamp": "2025-12-07T00:19:09.080842",
664
+ "type": "MARKET",
665
+ "symbol": "GOOGL",
666
+ "message": "\ud83d\udcc8 UP ALERT: GOOGL moved +2.33% to $161.03",
667
+ "details": {
668
+ "price": 161.03,
669
+ "change": 2.3257291732858847,
670
+ "timestamp": "2025-12-07 00:19:09"
671
+ }
672
+ },
673
+ {
674
+ "timestamp": "2025-12-07T00:19:08.583771",
675
+ "type": "MARKET",
676
+ "symbol": "MSFT",
677
+ "message": "\ud83d\udcc8 UP ALERT: MSFT moved +4.57% to $388.37",
678
+ "details": {
679
+ "price": 388.37,
680
+ "change": 4.574829016102108,
681
+ "timestamp": "2025-12-07 00:19:08"
682
+ }
683
+ },
684
+ {
685
+ "timestamp": "2025-12-07T00:19:08.356347",
686
+ "type": "MARKET",
687
+ "symbol": "NVDA",
688
+ "message": "\ud83d\udcc8 UP ALERT: NVDA moved +1.55% to $417.47",
689
+ "details": {
690
+ "price": 417.47,
691
+ "change": 1.5470312081924578,
692
+ "timestamp": "2025-12-07 00:19:08"
693
+ }
694
+ },
695
+ {
696
+ "timestamp": "2025-12-07T00:19:08.100527",
697
+ "type": "MARKET",
698
+ "symbol": "TSLA",
699
+ "message": "\ud83d\udcc9 DOWN ALERT: TSLA moved -0.67% to $232.45",
700
+ "details": {
701
+ "price": 232.45,
702
+ "change": -0.6666381778556483,
703
+ "timestamp": "2025-12-07 00:19:08"
704
+ }
705
+ },
706
+ {
707
+ "timestamp": "2025-12-07T00:19:07.845662",
708
+ "type": "MARKET",
709
+ "symbol": "AAPL",
710
+ "message": "\ud83d\udcc9 DOWN ALERT: AAPL moved -0.53% to $174.63",
711
+ "details": {
712
+ "price": 174.63,
713
+ "change": -0.5297334244702704,
714
+ "timestamp": "2025-12-07 00:19:07"
715
+ }
716
+ },
717
+ {
718
+ "timestamp": "2025-12-07T00:18:57.572888",
719
+ "type": "MARKET",
720
+ "symbol": "GOOGL",
721
+ "message": "\ud83d\udcc8 UP ALERT: GOOGL moved +5.89% to $154.70",
722
+ "details": {
723
+ "price": 154.7,
724
+ "change": 5.893627216099654,
725
+ "timestamp": "2025-12-07 00:18:57"
726
+ }
727
+ },
728
+ {
729
+ "timestamp": "2025-12-07T00:18:57.320075",
730
+ "type": "MARKET",
731
+ "symbol": "AMZN",
732
+ "message": "\ud83d\udcc8 UP ALERT: AMZN moved +2.62% to $152.36",
733
+ "details": {
734
+ "price": 152.36,
735
+ "change": 2.620057924159773,
736
+ "timestamp": "2025-12-07 00:18:57"
737
+ }
738
+ },
739
+ {
740
+ "timestamp": "2025-12-07T00:18:57.060315",
741
+ "type": "MARKET",
742
+ "symbol": "MSFT",
743
+ "message": "\ud83d\udcc8 UP ALERT: MSFT moved +2.68% to $490.81",
744
+ "details": {
745
+ "price": 490.81,
746
+ "change": 2.6842127285660453,
747
+ "timestamp": "2025-12-07 00:18:57"
748
+ }
749
+ },
750
+ {
751
+ "timestamp": "2025-12-07T00:18:56.597685",
752
+ "type": "MARKET",
753
+ "symbol": "TSLA",
754
+ "message": "\ud83d\udcc8 UP ALERT: TSLA moved +1.05% to $295.24",
755
+ "details": {
756
+ "price": 295.24,
757
+ "change": 1.0507581202724416,
758
+ "timestamp": "2025-12-07 00:18:56"
759
+ }
760
+ },
761
+ {
762
+ "timestamp": "2025-12-07T00:18:46.026021",
763
+ "type": "MARKET",
764
+ "symbol": "GOOGL",
765
+ "message": "\ud83d\udcc8 UP ALERT: GOOGL moved +5.89% to $154.70",
766
+ "details": {
767
+ "price": 154.7,
768
+ "change": 5.893627216099654,
769
+ "timestamp": "2025-12-07 00:18:46"
770
+ }
771
+ },
772
+ {
773
+ "timestamp": "2025-12-07T00:18:45.794249",
774
+ "type": "MARKET",
775
+ "symbol": "AMZN",
776
+ "message": "\ud83d\udcc8 UP ALERT: AMZN moved +2.62% to $152.36",
777
+ "details": {
778
+ "price": 152.36,
779
+ "change": 2.620057924159773,
780
+ "timestamp": "2025-12-07 00:18:45"
781
+ }
782
+ },
783
+ {
784
+ "timestamp": "2025-12-07T00:18:45.537097",
785
+ "type": "MARKET",
786
+ "symbol": "MSFT",
787
+ "message": "\ud83d\udcc8 UP ALERT: MSFT moved +2.68% to $490.81",
788
+ "details": {
789
+ "price": 490.81,
790
+ "change": 2.6842127285660453,
791
+ "timestamp": "2025-12-07 00:18:45"
792
+ }
793
+ },
794
+ {
795
+ "timestamp": "2025-12-07T00:18:45.050498",
796
+ "type": "MARKET",
797
+ "symbol": "TSLA",
798
+ "message": "\ud83d\udcc8 UP ALERT: TSLA moved +1.05% to $295.24",
799
+ "details": {
800
+ "price": 295.24,
801
+ "change": 1.0507581202724416,
802
+ "timestamp": "2025-12-07 00:18:45"
803
+ }
804
+ },
805
+ {
806
+ "timestamp": "2025-12-07T00:18:34.509053",
807
+ "type": "MARKET",
808
+ "symbol": "GOOGL",
809
+ "message": "\ud83d\udcc8 UP ALERT: GOOGL moved +5.89% to $154.70",
810
+ "details": {
811
+ "price": 154.7,
812
+ "change": 5.893627216099654,
813
+ "timestamp": "2025-12-07 00:18:34"
814
+ }
815
+ },
816
+ {
817
+ "timestamp": "2025-12-07T00:18:34.158647",
818
+ "type": "MARKET",
819
+ "symbol": "AMZN",
820
+ "message": "\ud83d\udcc8 UP ALERT: AMZN moved +2.62% to $152.36",
821
+ "details": {
822
+ "price": 152.36,
823
+ "change": 2.620057924159773,
824
+ "timestamp": "2025-12-07 00:18:34"
825
+ }
826
+ },
827
+ {
828
+ "timestamp": "2025-12-07T00:18:33.893424",
829
+ "type": "MARKET",
830
+ "symbol": "MSFT",
831
+ "message": "\ud83d\udcc8 UP ALERT: MSFT moved +2.68% to $490.81",
832
+ "details": {
833
+ "price": 490.81,
834
+ "change": 2.6842127285660453,
835
+ "timestamp": "2025-12-07 00:18:33"
836
+ }
837
+ },
838
+ {
839
+ "timestamp": "2025-12-07T00:18:33.330802",
840
+ "type": "MARKET",
841
+ "symbol": "TSLA",
842
+ "message": "\ud83d\udcc8 UP ALERT: TSLA moved +1.05% to $295.24",
843
+ "details": {
844
+ "price": 295.24,
845
+ "change": 1.0507581202724416,
846
+ "timestamp": "2025-12-07 00:18:33"
847
+ }
848
+ },
849
+ {
850
+ "timestamp": "2025-12-07T00:18:22.729714",
851
+ "type": "MARKET",
852
+ "symbol": "GOOGL",
853
+ "message": "\ud83d\udcc8 UP ALERT: GOOGL moved +5.89% to $154.70",
854
+ "details": {
855
+ "price": 154.7,
856
+ "change": 5.893627216099654,
857
+ "timestamp": "2025-12-07 00:18:22"
858
+ }
859
+ },
860
+ {
861
+ "timestamp": "2025-12-07T00:18:22.468028",
862
+ "type": "MARKET",
863
+ "symbol": "AMZN",
864
+ "message": "\ud83d\udcc8 UP ALERT: AMZN moved +2.62% to $152.36",
865
+ "details": {
866
+ "price": 152.36,
867
+ "change": 2.620057924159773,
868
+ "timestamp": "2025-12-07 00:18:22"
869
+ }
870
+ },
871
+ {
872
+ "timestamp": "2025-12-07T00:18:22.212773",
873
+ "type": "MARKET",
874
+ "symbol": "MSFT",
875
+ "message": "\ud83d\udcc8 UP ALERT: MSFT moved +2.68% to $490.81",
876
+ "details": {
877
+ "price": 490.81,
878
+ "change": 2.6842127285660453,
879
+ "timestamp": "2025-12-07 00:18:22"
880
+ }
881
+ },
882
+ {
883
+ "timestamp": "2025-12-07T00:18:21.714340",
884
+ "type": "MARKET",
885
+ "symbol": "TSLA",
886
+ "message": "\ud83d\udcc8 UP ALERT: TSLA moved +1.05% to $295.24",
887
+ "details": {
888
+ "price": 295.24,
889
+ "change": 1.0507581202724416,
890
+ "timestamp": "2025-12-07 00:18:21"
891
+ }
892
+ },
893
+ {
894
+ "timestamp": "2025-12-07T00:18:11.153845",
895
+ "type": "MARKET",
896
+ "symbol": "GOOGL",
897
+ "message": "\ud83d\udcc8 UP ALERT: GOOGL moved +5.89% to $154.70",
898
+ "details": {
899
+ "price": 154.7,
900
+ "change": 5.893627216099654,
901
+ "timestamp": "2025-12-07 00:18:11"
902
+ }
903
+ },
904
+ {
905
+ "timestamp": "2025-12-07T00:18:10.897046",
906
+ "type": "MARKET",
907
+ "symbol": "AMZN",
908
+ "message": "\ud83d\udcc8 UP ALERT: AMZN moved +2.62% to $152.36",
909
+ "details": {
910
+ "price": 152.36,
911
+ "change": 2.620057924159773,
912
+ "timestamp": "2025-12-07 00:18:10"
913
+ }
914
+ },
915
+ {
916
+ "timestamp": "2025-12-07T00:18:10.638176",
917
+ "type": "MARKET",
918
+ "symbol": "MSFT",
919
+ "message": "\ud83d\udcc8 UP ALERT: MSFT moved +2.68% to $490.81",
920
+ "details": {
921
+ "price": 490.81,
922
+ "change": 2.6842127285660453,
923
+ "timestamp": "2025-12-07 00:18:10"
924
+ }
925
+ },
926
+ {
927
+ "timestamp": "2025-12-07T00:18:10.065308",
928
+ "type": "MARKET",
929
+ "symbol": "TSLA",
930
+ "message": "\ud83d\udcc8 UP ALERT: TSLA moved +1.05% to $295.24",
931
+ "details": {
932
+ "price": 295.24,
933
+ "change": 1.0507581202724416,
934
+ "timestamp": "2025-12-07 00:18:10"
935
+ }
936
+ },
937
+ {
938
+ "timestamp": "2025-12-07T00:17:59.312775",
939
+ "type": "MARKET",
940
+ "symbol": "GOOGL",
941
+ "message": "\ud83d\udcc8 UP ALERT: GOOGL moved +0.86% to $147.04",
942
+ "details": {
943
+ "price": 147.04,
944
+ "change": 0.8573976267233693,
945
+ "timestamp": "2025-12-07 00:17:59"
946
+ }
947
+ },
948
+ {
949
+ "timestamp": "2025-12-07T00:17:59.073640",
950
+ "type": "MARKET",
951
+ "symbol": "AMZN",
952
+ "message": "\ud83d\udcc8 UP ALERT: AMZN moved +1.69% to $181.88",
953
+ "details": {
954
+ "price": 181.88,
955
+ "change": 1.6941571149007555,
956
+ "timestamp": "2025-12-07 00:17:59"
957
+ }
958
+ },
959
+ {
960
+ "timestamp": "2025-12-07T00:17:58.765582",
961
+ "type": "MARKET",
962
+ "symbol": "MSFT",
963
+ "message": "\ud83d\udcc8 UP ALERT: MSFT moved +3.55% to $417.01",
964
+ "details": {
965
+ "price": 417.01,
966
+ "change": 3.5457999155761857,
967
+ "timestamp": "2025-12-07 00:17:58"
968
+ }
969
+ },
970
+ {
971
+ "timestamp": "2025-12-07T00:17:58.535908",
972
+ "type": "MARKET",
973
+ "symbol": "NVDA",
974
+ "message": "\ud83d\udcc9 DOWN ALERT: NVDA moved -2.75% to $352.00",
975
+ "details": {
976
+ "price": 352.0,
977
+ "change": -2.748998480453098,
978
+ "timestamp": "2025-12-07 00:17:58"
979
+ }
980
+ },
981
+ {
982
+ "timestamp": "2025-12-07T00:17:58.135874",
983
+ "type": "MARKET",
984
+ "symbol": "TSLA",
985
+ "message": "\ud83d\udcc8 UP ALERT: TSLA moved +2.70% to $293.18",
986
+ "details": {
987
+ "price": 293.18,
988
+ "change": 2.7044069221607328,
989
+ "timestamp": "2025-12-07 00:17:57"
990
+ }
991
+ },
992
+ {
993
+ "timestamp": "2025-12-07T00:17:45.561204",
994
+ "type": "MARKET",
995
+ "symbol": "GOOGL",
996
+ "message": "\ud83d\udcc8 UP ALERT: GOOGL moved +0.86% to $147.04",
997
+ "details": {
998
+ "price": 147.04,
999
+ "change": 0.8573976267233693,
1000
+ "timestamp": "2025-12-07 00:17:45"
1001
+ }
1002
+ },
1003
+ {
1004
+ "timestamp": "2025-12-07T00:17:45.294768",
1005
+ "type": "MARKET",
1006
+ "symbol": "AMZN",
1007
+ "message": "\ud83d\udcc8 UP ALERT: AMZN moved +1.69% to $181.88",
1008
+ "details": {
1009
+ "price": 181.88,
1010
+ "change": 1.6941571149007555,
1011
+ "timestamp": "2025-12-07 00:17:45"
1012
+ }
1013
+ },
1014
+ {
1015
+ "timestamp": "2025-12-07T00:17:45.029167",
1016
+ "type": "MARKET",
1017
+ "symbol": "MSFT",
1018
+ "message": "\ud83d\udcc8 UP ALERT: MSFT moved +3.55% to $417.01",
1019
+ "details": {
1020
+ "price": 417.01,
1021
+ "change": 3.5457999155761857,
1022
+ "timestamp": "2025-12-07 00:17:45"
1023
+ }
1024
+ },
1025
+ {
1026
+ "timestamp": "2025-12-07T00:17:44.757997",
1027
+ "type": "MARKET",
1028
+ "symbol": "NVDA",
1029
+ "message": "\ud83d\udcc9 DOWN ALERT: NVDA moved -2.75% to $352.00",
1030
+ "details": {
1031
+ "price": 352.0,
1032
+ "change": -2.748998480453098,
1033
+ "timestamp": "2025-12-07 00:17:44"
1034
+ }
1035
+ },
1036
+ {
1037
+ "timestamp": "2025-12-07T00:17:44.462497",
1038
+ "type": "MARKET",
1039
+ "symbol": "TSLA",
1040
+ "message": "\ud83d\udcc8 UP ALERT: TSLA moved +2.70% to $293.18",
1041
+ "details": {
1042
+ "price": 293.18,
1043
+ "change": 2.7044069221607328,
1044
+ "timestamp": "2025-12-07 00:17:44"
1045
+ }
1046
+ },
1047
+ {
1048
+ "timestamp": "2025-12-07T00:17:33.807942",
1049
+ "type": "MARKET",
1050
+ "symbol": "GOOGL",
1051
+ "message": "\ud83d\udcc8 UP ALERT: GOOGL moved +0.86% to $147.04",
1052
+ "details": {
1053
+ "price": 147.04,
1054
+ "change": 0.8573976267233693,
1055
+ "timestamp": "2025-12-07 00:17:33"
1056
+ }
1057
+ },
1058
+ {
1059
+ "timestamp": "2025-12-07T00:17:33.570368",
1060
+ "type": "MARKET",
1061
+ "symbol": "AMZN",
1062
+ "message": "\ud83d\udcc8 UP ALERT: AMZN moved +1.69% to $181.88",
1063
+ "details": {
1064
+ "price": 181.88,
1065
+ "change": 1.6941571149007555,
1066
+ "timestamp": "2025-12-07 00:17:33"
1067
+ }
1068
+ },
1069
+ {
1070
+ "timestamp": "2025-12-07T00:17:33.327027",
1071
+ "type": "MARKET",
1072
+ "symbol": "MSFT",
1073
+ "message": "\ud83d\udcc8 UP ALERT: MSFT moved +3.55% to $417.01",
1074
+ "details": {
1075
+ "price": 417.01,
1076
+ "change": 3.5457999155761857,
1077
+ "timestamp": "2025-12-07 00:17:33"
1078
+ }
1079
+ },
1080
+ {
1081
+ "timestamp": "2025-12-07T00:17:33.053890",
1082
+ "type": "MARKET",
1083
+ "symbol": "NVDA",
1084
+ "message": "\ud83d\udcc9 DOWN ALERT: NVDA moved -2.75% to $352.00",
1085
+ "details": {
1086
+ "price": 352.0,
1087
+ "change": -2.748998480453098,
1088
+ "timestamp": "2025-12-07 00:17:33"
1089
+ }
1090
+ },
1091
+ {
1092
+ "timestamp": "2025-12-07T00:17:32.778805",
1093
+ "type": "MARKET",
1094
+ "symbol": "TSLA",
1095
+ "message": "\ud83d\udcc8 UP ALERT: TSLA moved +2.70% to $293.18",
1096
+ "details": {
1097
+ "price": 293.18,
1098
+ "change": 2.7044069221607328,
1099
+ "timestamp": "2025-12-07 00:17:32"
1100
+ }
1101
+ }
1102
+ ]
alphavantage_mcp.py ADDED
@@ -0,0 +1,213 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # alphavantage_mcp.py (Corrected for Free Tier)
2
+ from fastapi import FastAPI, HTTPException
3
+ import uvicorn
4
+ import os
5
+ from dotenv import load_dotenv
6
+ from alpha_vantage.timeseries import TimeSeries
7
+ import logging
8
+
9
+ # --- Configuration ---
10
+ load_dotenv()
11
+
12
+ # --- Logging Setup (MUST be before we use logger) ---
13
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
14
+ logger = logging.getLogger("AlphaVantage_MCP_Server")
15
+
16
+ # --- Get API Key ---
17
+ ALPHA_VANTAGE_API_KEY = os.getenv("ALPHA_VANTAGE_API_KEY")
18
+
19
+ # Fallback: Try to read from Streamlit secrets file (for cloud deployment)
20
+ if not ALPHA_VANTAGE_API_KEY:
21
+ try:
22
+ import toml
23
+ secrets_path = os.path.join(os.path.dirname(__file__), ".streamlit", "secrets.toml")
24
+ if os.path.exists(secrets_path):
25
+ secrets = toml.load(secrets_path)
26
+ ALPHA_VANTAGE_API_KEY = secrets.get("ALPHA_VANTAGE_API_KEY")
27
+ logger.info("Loaded ALPHA_VANTAGE_API_KEY from .streamlit/secrets.toml")
28
+ except Exception as e:
29
+ logger.warning(f"Could not load from secrets.toml: {e}")
30
+
31
+ if not ALPHA_VANTAGE_API_KEY:
32
+ logger.warning("ALPHA_VANTAGE_API_KEY not found in environment. Market data features will fail.")
33
+ else:
34
+ logger.info(f"ALPHA_VANTAGE_API_KEY found: {ALPHA_VANTAGE_API_KEY[:4]}...")
35
+
36
+ # --- FastAPI App & Alpha Vantage Client ---
37
+ app = FastAPI(title="Aegis Alpha Vantage MCP Server")
38
+ ts = TimeSeries(key=ALPHA_VANTAGE_API_KEY, output_format='json')
39
+
40
+ @app.post("/market_data")
41
+ async def get_market_data(payload: dict):
42
+ """
43
+ Fetches market data using the Alpha Vantage API.
44
+ Supports both intraday and daily data based on time_range.
45
+ Expects a payload like:
46
+ {
47
+ "symbol": "NVDA",
48
+ "time_range": "INTRADAY" | "1D" | "3D" | "1W" | "1M" | "3M" | "1Y"
49
+ }
50
+ """
51
+ symbol = payload.get("symbol")
52
+ time_range = payload.get("time_range", "INTRADAY")
53
+
54
+ if not symbol:
55
+ logger.error("Validation Error: 'symbol' is required.")
56
+ raise HTTPException(status_code=400, detail="'symbol' is required.")
57
+
58
+ logger.info(f"Received market data request for symbol: {symbol}, time_range: {time_range}")
59
+
60
+ try:
61
+ # Route to appropriate API based on time range
62
+ if time_range == "INTRADAY":
63
+ # Intraday data (last 4-6 hours, 5-min intervals)
64
+ data, meta_data = ts.get_intraday(symbol=symbol, interval="5min", outputsize='compact')
65
+ logger.info(f"Successfully retrieved intraday data for {symbol}")
66
+ meta_data["Source"] = "Real API (Alpha Vantage)"
67
+ else:
68
+ # Daily data for historical ranges
69
+ data, meta_data = ts.get_daily(symbol=symbol, outputsize='full')
70
+ logger.info(f"Successfully retrieved daily data for {symbol}")
71
+
72
+ # Filter data based on time range
73
+ data = filter_data_by_time_range(data, time_range)
74
+ logger.info(f"Filtered to {len(data)} data points for time_range={time_range}")
75
+ meta_data["Source"] = "Real API (Alpha Vantage)"
76
+
77
+ return {"status": "success", "data": data, "meta_data": meta_data}
78
+
79
+ except Exception as e:
80
+ # Catch ALL exceptions to ensure fallback works
81
+ logger.error(f"Alpha Vantage API error for symbol {symbol}: {e}")
82
+ logger.warning(f"Triggering MOCK DATA fallback for {symbol} due to error.")
83
+
84
+ import random
85
+ import math
86
+ from datetime import datetime, timedelta
87
+
88
+ # Seed randomness with symbol AND date to ensure it changes daily
89
+ # But stays consistent within the same day
90
+ today_str = datetime.now().strftime("%Y-%m-%d %H:%M")
91
+ seed_value = f"{symbol}_{today_str}"
92
+ random.seed(seed_value)
93
+
94
+ mock_data = {}
95
+ current_time = datetime.now()
96
+
97
+ # Generate unique base price
98
+ symbol_hash = sum(ord(c) for c in symbol)
99
+ base_price = float(symbol_hash % 500) + 50
100
+
101
+ # Force distinct start prices for common stocks
102
+ if "AAPL" in symbol: base_price = 150.0
103
+ if "TSLA" in symbol: base_price = 250.0
104
+ if "NVDA" in symbol: base_price = 450.0
105
+ if "MSFT" in symbol: base_price = 350.0
106
+ if "GOOG" in symbol: base_price = 130.0
107
+ if "AMZN" in symbol: base_price = 140.0
108
+
109
+ # Add some daily variation to base price
110
+ daily_noise = (hash(today_str) % 100) / 10.0 # -5 to +5 variation
111
+ base_price += daily_noise
112
+
113
+ trend_direction = 1 if symbol_hash % 2 == 0 else -1
114
+ volatility = base_price * 0.02
115
+ trend_strength = base_price * 0.001
116
+ current_price = base_price
117
+
118
+ # Determine number of data points based on time range
119
+ if time_range == "INTRADAY":
120
+ num_points = 100
121
+ time_delta = timedelta(minutes=5)
122
+ elif time_range in ["1D", "3D"]:
123
+ num_points = int(time_range[0]) if time_range != "1D" else 1
124
+ time_delta = timedelta(days=1)
125
+ elif time_range == "1W":
126
+ num_points = 7
127
+ time_delta = timedelta(days=1)
128
+ elif time_range == "1M":
129
+ num_points = 30
130
+ time_delta = timedelta(days=1)
131
+ elif time_range == "3M":
132
+ num_points = 90
133
+ time_delta = timedelta(days=1)
134
+ elif time_range == "1Y":
135
+ num_points = 365
136
+ time_delta = timedelta(days=1)
137
+ else:
138
+ num_points = 100
139
+ time_delta = timedelta(minutes=5)
140
+
141
+ for i in range(num_points):
142
+ noise = random.uniform(-volatility, volatility)
143
+ cycle_1 = (base_price * 0.02) * math.sin(i / 8.0)
144
+ cycle_2 = (base_price * 0.01) * math.sin(i / 3.0)
145
+ change = noise + (trend_direction * trend_strength)
146
+ current_price += change
147
+ final_price = current_price + cycle_1 + cycle_2
148
+ final_price = max(1.0, final_price)
149
+
150
+ t = current_time - (time_delta * (num_points - i - 1))
151
+
152
+ # Format timestamp based on data type
153
+ if time_range == "INTRADAY":
154
+ timestamp_str = t.strftime("%Y-%m-%d %H:%M:%S")
155
+ else:
156
+ timestamp_str = t.strftime("%Y-%m-%d")
157
+
158
+ mock_data[timestamp_str] = {
159
+ "1. open": str(round(final_price, 2)),
160
+ "2. high": str(round(final_price + (volatility * 0.3), 2)),
161
+ "3. low": str(round(final_price - (volatility * 0.3), 2)),
162
+ "4. close": str(round(final_price + random.uniform(-0.1, 0.1), 2)),
163
+ "5. volume": str(int(random.uniform(100000, 5000000)))
164
+ }
165
+
166
+ return {
167
+ "status": "success",
168
+ "data": mock_data,
169
+ "meta_data": {
170
+ "Information": f"Mock Data ({time_range}) - API Limit/Error",
171
+ "Source": "Simulated (Fallback)"
172
+ }
173
+ }
174
+
175
+
176
+ def filter_data_by_time_range(data: dict, time_range: str) -> dict:
177
+ """Filter daily data to the specified time range."""
178
+ from datetime import datetime, timedelta
179
+
180
+ # Map time ranges to days
181
+ range_map = {
182
+ "1D": 1,
183
+ "3D": 3,
184
+ "1W": 7,
185
+ "1M": 30,
186
+ "3M": 90,
187
+ "1Y": 365
188
+ }
189
+
190
+ days = range_map.get(time_range, 30)
191
+ cutoff_date = datetime.now() - timedelta(days=days)
192
+
193
+ # Filter data
194
+ filtered = {}
195
+ for timestamp_str, values in data.items():
196
+ try:
197
+ timestamp = datetime.strptime(timestamp_str, "%Y-%m-%d")
198
+ if timestamp >= cutoff_date:
199
+ filtered[timestamp_str] = values
200
+ except:
201
+ # If parsing fails, include the data point
202
+ filtered[timestamp_str] = values
203
+
204
+ return filtered
205
+
206
+
207
+ @app.get("/")
208
+ def read_root():
209
+ return {"message": "Aegis Alpha Vantage MCP Server is operational."}
210
+
211
+ # --- Main Execution ---
212
+ if __name__ == "__main__":
213
+ uvicorn.run(app, host="127.0.0.1", port=8002)
app.py ADDED
@@ -0,0 +1,407 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import sys
3
+ import os
4
+ import httpx
5
+ import pandas as pd
6
+ import json
7
+ import time
8
+ from datetime import datetime
9
+ import base64
10
+ import subprocess
11
+
12
+ # --- Path Setup ---
13
+ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '.')))
14
+
15
+ # --- Configuration ---
16
+ WATCHLIST_FILE = "watchlist.json"
17
+ ALERTS_FILE = "alerts.json"
18
+
19
+ # --- Page Configuration ---
20
+ st.set_page_config(
21
+ page_title="Sentinel - AI Financial Intelligence",
22
+ page_icon="🛡️",
23
+ layout="wide",
24
+ initial_sidebar_state="expanded"
25
+ )
26
+
27
+ # --- Custom CSS ---
28
+ def load_css(file_name):
29
+ with open(file_name) as f:
30
+ st.markdown(f'<style>{f.read()}</style>', unsafe_allow_html=True)
31
+
32
+ load_css("style.css")
33
+
34
+ # --- Auto-Start Backend Services ---
35
+ # --- Auto-Start Backend Services ---
36
+ # --- Auto-Start Backend Services ---
37
+ @st.cache_resource
38
+ def start_background_services():
39
+ # Managed by main.py in production
40
+ pass
41
+
42
+ # Trigger startup (cached, runs once per container)
43
+ start_background_services()
44
+
45
+ # --- Helper Functions ---
46
+ @st.cache_data(ttl=60)
47
+ def check_server_status():
48
+ urls = {"Gateway": "http://127.0.0.1:8000/", "Tavily": "http://127.0.0.1:8001/", "Alpha Vantage": "http://127.0.0.1:8002/", "Private DB": "http://127.0.0.1:8003/"}
49
+ statuses = {}
50
+ with httpx.Client(timeout=2.0) as client:
51
+ for name, url in urls.items():
52
+ try:
53
+ response = client.get(url)
54
+ statuses[name] = "✅ Online" if response.status_code == 200 else "⚠️ Error"
55
+ except: statuses[name] = "❌ Offline"
56
+ return statuses
57
+
58
+ def load_watchlist():
59
+ if not os.path.exists(WATCHLIST_FILE): return []
60
+ try:
61
+ with open(WATCHLIST_FILE, 'r') as f:
62
+ return json.load(f)
63
+ except:
64
+ return []
65
+
66
+ def save_watchlist(watchlist):
67
+ with open(WATCHLIST_FILE, 'w') as f: json.dump(watchlist, f)
68
+
69
+ def load_alerts():
70
+ if not os.path.exists(ALERTS_FILE): return []
71
+ try:
72
+ with open(ALERTS_FILE, 'r') as f:
73
+ return json.load(f)
74
+ except:
75
+ return []
76
+
77
+ def get_base64_image(image_path):
78
+ try:
79
+ with open(image_path, "rb") as img_file:
80
+ return base64.b64encode(img_file.read()).decode()
81
+ except Exception:
82
+ return ""
83
+
84
+ # --- Session State ---
85
+ if 'page' not in st.session_state:
86
+ st.session_state.page = 'home'
87
+ if 'analysis_complete' not in st.session_state:
88
+ st.session_state.analysis_complete = False
89
+ if 'final_state' not in st.session_state:
90
+ st.session_state.final_state = None
91
+ if 'error_message' not in st.session_state:
92
+ st.session_state.error_message = None
93
+
94
+ # --- UI Components ---
95
+
96
+ def render_sidebar():
97
+ with st.sidebar:
98
+ # Logo Area
99
+ logo_base64 = get_base64_image("assets/logo.png")
100
+ if logo_base64:
101
+ st.markdown(f"""
102
+ <div style="text-align: center; margin-bottom: 2rem;">
103
+ <img src="data:image/png;base64,{logo_base64}" style="width: 80px; height: 80px; margin-bottom: 10px;">
104
+ <h2 style="margin:0; font-size: 1.5rem;">SENTINEL</h2>
105
+ <p style="color: var(--text-secondary); font-size: 0.8rem;">AI Financial Intelligence</p>
106
+ </div>
107
+ """, unsafe_allow_html=True)
108
+
109
+ # Navigation
110
+ if st.button("🏠 Home", use_container_width=True):
111
+ st.session_state.page = 'home'
112
+ st.rerun()
113
+
114
+ if st.button("⚡ Analysis Console", use_container_width=True):
115
+ st.session_state.page = 'analysis'
116
+ st.rerun()
117
+
118
+ st.markdown("---")
119
+
120
+ # Settings - Completely Redesigned
121
+ st.markdown("### 🎯 Intelligence Configuration")
122
+
123
+ # Analysis Depth
124
+ st.select_slider(
125
+ "Analysis Depth",
126
+ options=["Quick Scan", "Standard", "Deep Dive", "Comprehensive"],
127
+ value="Standard"
128
+ )
129
+
130
+ # Risk Profile
131
+ st.selectbox(
132
+ "Risk Tolerance",
133
+ ["Conservative", "Moderate", "Aggressive", "Custom"],
134
+ help="Adjusts recommendation thresholds"
135
+ )
136
+
137
+ # Time Horizon
138
+ st.radio(
139
+ "Investment Horizon",
140
+ ["Short-term (< 1 year)", "Medium-term (1-5 years)", "Long-term (5+ years)"],
141
+ index=1
142
+ )
143
+
144
+ # Market Sentiment Tracking
145
+ st.toggle("Track Market Sentiment", value=True, help="Include social media and news sentiment analysis")
146
+
147
+ st.markdown("---")
148
+
149
+ # System Status
150
+ with st.expander("📡 System Status", expanded=False):
151
+ server_statuses = check_server_status()
152
+ for name, status in server_statuses.items():
153
+ dot_class = "status-ok" if status == "✅ Online" else "status-err"
154
+ st.markdown(f"""
155
+ <div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px;">
156
+ <span style="font-size: 0.9rem;">{name}</span>
157
+ <div><span class="status-dot {dot_class}"></span><span style="font-size: 0.8rem; color: var(--text-secondary);">{status.split(' ')[1]}</span></div>
158
+ </div>
159
+ """, unsafe_allow_html=True)
160
+
161
+ # Watchlist
162
+ with st.expander("🛡️ Watchlist", expanded=False):
163
+ watchlist = load_watchlist()
164
+ new_symbol = st.text_input("Add Symbol:", placeholder="e.g. MSFT").upper()
165
+ if st.button("Add"):
166
+ if new_symbol and new_symbol not in watchlist:
167
+ watchlist.append(new_symbol)
168
+ save_watchlist(watchlist)
169
+ st.rerun()
170
+
171
+ if watchlist:
172
+ st.markdown("---")
173
+ for symbol in watchlist:
174
+ col1, col2 = st.columns([3, 1])
175
+ col1.markdown(f"**{symbol}**")
176
+ if col2.button("❌", key=f"del_{symbol}"):
177
+ watchlist.remove(symbol)
178
+ save_watchlist(watchlist)
179
+ st.rerun()
180
+
181
+ def render_home():
182
+ # Auto-refresh logic (Every 10s)
183
+ if 'last_refresh_home' not in st.session_state:
184
+ st.session_state.last_refresh_home = time.time()
185
+
186
+ if time.time() - st.session_state.last_refresh_home > 10:
187
+ st.session_state.last_refresh_home = time.time()
188
+ st.rerun()
189
+
190
+ # Hero Section with Logo
191
+ logo_base64 = get_base64_image("assets/logo.png")
192
+
193
+ if logo_base64:
194
+ st.markdown(f"""
195
+ <div class="hero-container">
196
+ <div style="display: flex; align-items: center; justify-content: center; gap: 20px; margin-bottom: 1.5rem;">
197
+ <img src="data:image/png;base64,{logo_base64}" style="width: 80px; height: 80px;">
198
+ <h1 class="hero-title" style="margin: 0;">Sentinel AI<br>Financial Intelligence</h1>
199
+ </div>
200
+ <p class="hero-subtitle">
201
+ Transform raw market data into actionable business insights with the power of AI.
202
+ Analyze stocks, news, and portfolios automatically using intelligent agents.
203
+ </p>
204
+ </div>
205
+ """, unsafe_allow_html=True)
206
+ else:
207
+ # Fallback without logo
208
+ st.markdown("""
209
+ <div class="hero-container">
210
+ <h1 class="hero-title">Sentinel AI<br>Financial Intelligence</h1>
211
+ <p class="hero-subtitle">
212
+ Transform raw market data into actionable business insights with the power of AI.
213
+ Analyze stocks, news, and portfolios automatically using intelligent agents.
214
+ </p>
215
+ </div>
216
+ """, unsafe_allow_html=True)
217
+
218
+ col1, col2, col3 = st.columns([1, 2, 1])
219
+ with col2:
220
+ if st.button("🚀 Start Analysis", use_container_width=True):
221
+ st.session_state.page = 'analysis'
222
+ st.rerun()
223
+
224
+ # Feature Cards
225
+ st.markdown("""
226
+ <div class="feature-grid">
227
+ <div class="feature-card">
228
+ <div class="feature-icon">🧠</div>
229
+ <div class="feature-title">Intelligent Analysis</div>
230
+ <div class="feature-desc">
231
+ Our AI automatically understands market structures, identifies patterns, and generates meaningful insights without manual configuration.
232
+ </div>
233
+ </div>
234
+ <div class="feature-card">
235
+ <div class="feature-icon">📊</div>
236
+ <div class="feature-title">Smart Visualizations</div>
237
+ <div class="feature-desc">
238
+ Intelligently creates the most appropriate charts and graphs for your data, with interactive visualizations.
239
+ </div>
240
+ </div>
241
+ <div class="feature-card">
242
+ <div class="feature-icon">🎯</div>
243
+ <div class="feature-title">Actionable Recommendations</div>
244
+ <div class="feature-desc">
245
+ Get specific, measurable recommendations for improving your portfolio based on data-driven insights.
246
+ </div>
247
+ </div>
248
+ </div>
249
+ """, unsafe_allow_html=True)
250
+
251
+ # --- Live Wire on Home Page ---
252
+ st.markdown("---")
253
+ st.markdown("### 🚨 Live Wire Trending")
254
+
255
+ alerts_container = st.container()
256
+ alerts = load_alerts()
257
+ if not alerts:
258
+ alerts_container.caption("No active alerts in feed.")
259
+ else:
260
+ for alert in reversed(alerts[-10:]): # Show last 10 on home
261
+ alert_type = alert.get("type", "INFO")
262
+ css_class = "alert-market" if alert_type == "MARKET" else "alert-news" if alert_type == "NEWS" else ""
263
+ icon = "📉" if alert_type == "MARKET" else "📰"
264
+ timestamp = datetime.fromisoformat(alert.get("timestamp", datetime.now().isoformat())).strftime("%H:%M:%S")
265
+
266
+ html = f"""
267
+ <div class="alert-card {css_class}">
268
+ <div class="alert-header">
269
+ <span>{icon} {alert.get("symbol")}</span>
270
+ <span>{timestamp}</span>
271
+ </div>
272
+ <div class="alert-body">
273
+ {alert.get("message")}
274
+ </div>
275
+ </div>
276
+ """
277
+ alerts_container.markdown(html, unsafe_allow_html=True)
278
+
279
+ # Footer
280
+ st.markdown("<br><br><br>", unsafe_allow_html=True)
281
+ st.markdown("""
282
+ <div style="text-align: center; color: var(--text-secondary); font-size: 0.9rem;">
283
+ Powered by <b>Google Gemini</b> • Built with <b>LangGraph</b> • Designed with <b>Streamlit</b>
284
+ </div>
285
+ """, unsafe_allow_html=True)
286
+
287
+ def render_analysis():
288
+ st.markdown("## ⚡ Intelligence Directive")
289
+
290
+ # Error Display
291
+ if st.session_state.error_message:
292
+ st.error(st.session_state.error_message)
293
+ if st.button("Dismiss Error"):
294
+ st.session_state.error_message = None
295
+ st.rerun()
296
+
297
+ col_main, col_alerts = st.columns([3, 1.2])
298
+
299
+ with col_main:
300
+ with st.form("research_form", clear_on_submit=False):
301
+ task_input = st.text_area("Enter directive:", placeholder="e.g., Analyze the recent volatility for Tesla ($TSLA) and summarize news.", height=100)
302
+ submitted = st.form_submit_button("EXECUTE ANALYSIS", use_container_width=True)
303
+
304
+ if submitted and task_input:
305
+ st.session_state.error_message = None
306
+ server_statuses = check_server_status()
307
+ all_online = all(s == "✅ Online" for s in server_statuses.values())
308
+
309
+ if not all_online:
310
+ st.error("SYSTEM HALTED: Core services offline. Check sidebar status.")
311
+ else:
312
+ with st.status("🚀 SENTINEL ORCHESTRATOR ENGAGED...", expanded=True) as status:
313
+ try:
314
+ from agents.orchestrator_v3 import get_orchestrator
315
+ # Use default provider or env var
316
+ orchestrator = get_orchestrator(llm_provider="gemini")
317
+
318
+ final_state_result = {}
319
+ for event in orchestrator.stream({"task": task_input}):
320
+ agent_name = list(event.keys())[0]
321
+ state_update = list(event.values())[0]
322
+ final_state_result.update(state_update)
323
+
324
+ status.write(f"🛡️ Agent Active: {agent_name}...")
325
+
326
+ status.update(label="✅ Analysis Complete!", state="complete", expanded=False)
327
+ st.session_state.final_state = final_state_result
328
+ st.session_state.analysis_complete = True
329
+ st.rerun()
330
+ except Exception as e:
331
+ status.update(label="❌ System Failure", state="error")
332
+ st.session_state.error_message = f"RUNTIME ERROR: {e}"
333
+ st.rerun()
334
+
335
+ if st.session_state.analysis_complete:
336
+ final_state = st.session_state.final_state
337
+ symbol = final_state.get('symbol', 'N/A') if final_state else 'N/A'
338
+
339
+ st.markdown(f"### 📝 Report: {symbol}")
340
+
341
+ # Executive Summary
342
+ st.info(final_state.get("final_report", "No report generated."))
343
+
344
+ # Deep-Dive Insights
345
+ with st.expander("🔍 Deep-Dive Insights", expanded=True):
346
+ insights = final_state.get("analysis_results", {}).get("insights")
347
+ if insights: st.markdown(insights)
348
+ else: st.warning("No deep-dive insights available.")
349
+
350
+ # Charts
351
+ with st.expander("📊 Market Telemetry"):
352
+ charts = final_state.get("analysis_results", {}).get("charts", [])
353
+ if charts:
354
+ for chart in charts:
355
+ st.plotly_chart(chart, use_container_width=True)
356
+ else:
357
+ st.caption("No telemetry data available.")
358
+
359
+ # Raw Data
360
+ with st.expander("💾 Raw Intelligence Logs"):
361
+ tab1, tab2, tab3 = st.tabs(["Web Intelligence", "Market Data", "Internal Portfolio"])
362
+ with tab1: st.json(final_state.get('web_research_results', '{}'))
363
+ with tab2: st.json(final_state.get('market_data_results', '{}'))
364
+ with tab3: st.json(final_state.get('portfolio_data_results', '{}'))
365
+
366
+ if st.button("🛡️ New Analysis"):
367
+ st.session_state.analysis_complete = False
368
+ st.session_state.final_state = None
369
+ st.rerun()
370
+
371
+ # Live Alerts Feed
372
+ with col_alerts:
373
+ st.markdown("### 🚨 Live Wire")
374
+ alerts_container = st.container()
375
+
376
+ # Auto-refresh logic
377
+ if 'last_refresh' not in st.session_state:
378
+ st.session_state.last_refresh = time.time()
379
+
380
+ if time.time() - st.session_state.last_refresh > 10:
381
+ st.session_state.last_refresh = time.time()
382
+ st.rerun()
383
+
384
+ alerts = load_alerts()
385
+ if not alerts:
386
+ alerts_container.caption("No active alerts in feed.")
387
+ else:
388
+ for alert in reversed(alerts[-20:]):
389
+ alert_type = alert.get("type", "INFO")
390
+ css_class = "alert-market" if alert_type == "MARKET" else "alert-news" if alert_type == "NEWS" else ""
391
+ icon = "📉" if alert_type == "MARKET" else "📰"
392
+ timestamp = datetime.fromisoformat(alert.get("timestamp", datetime.now().isoformat())).strftime("%H:%M:%S")
393
+
394
+ html = f"""
395
+ <div class="alert-card {css_class}">
396
+ <div class="alert-header">
397
+ <span>{icon} {alert.get("symbol")}</span>
398
+ <span>{timestamp}</span>
399
+ </div>
400
+ <div class="alert-body">
401
+ {alert.get("message")}
402
+ </div>
403
+ </div>
404
+ """
405
+ alerts_container.markdown(html, unsafe_allow_html=True)
406
+
407
+ render_analysis()
app_command_center.py ADDED
@@ -0,0 +1,318 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app_command_center.py
2
+ import streamlit as st
3
+ import sys
4
+ import os
5
+ import httpx
6
+ import time
7
+ import json
8
+ from datetime import datetime
9
+
10
+ # --- Path Setup ---
11
+ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '.')))
12
+ from agents.orchestrator_v3 import SentinelOrchestratorV3
13
+
14
+ # --- Configuration ---
15
+ WATCHLIST_FILE = "watchlist.json"
16
+ ALERTS_FILE = "alerts.json"
17
+
18
+ # --- Page Configuration ---
19
+ st.set_page_config(
20
+ page_title="Aegis Digital Briefing",
21
+ page_icon="🛡️",
22
+ layout="wide"
23
+ )
24
+
25
+ # --- Custom CSS for the Briefing Room Theme ---
26
+ st.markdown("""
27
+ <style>
28
+ @import url('https://fonts.googleapis.com/css2?family=Source+Serif+4:wght@600;700&family=Open+Sans:wght@400;600&display=swap');
29
+
30
+ html, body, [class*="st-"] {
31
+ font-family: 'Open Sans', sans-serif;
32
+ }
33
+
34
+ /* Main Headers */
35
+ h1, h2, h3 {
36
+ font-family: 'Source Serif 4', serif;
37
+ }
38
+ .main-header {
39
+ font-size: 2.8rem;
40
+ font-weight: 700;
41
+ color: #1A202C;
42
+ text-align: center;
43
+ margin-bottom: 0.5rem;
44
+ }
45
+ .subtitle {
46
+ text-align: center;
47
+ color: #718096;
48
+ font-size: 1.1rem;
49
+ margin-bottom: 2.5rem;
50
+ }
51
+
52
+ /* Card/Widget styling */
53
+ .card {
54
+ background-color: #FFFFFF;
55
+ border-radius: 8px;
56
+ padding: 25px;
57
+ border: 1px solid #E2E8F0;
58
+ }
59
+ .metric-card {
60
+ border-radius: 8px;
61
+ padding: 1.5rem;
62
+ text-align: center;
63
+ border: 1px solid #E2E8F0;
64
+ }
65
+ .metric-value {
66
+ font-size: 2rem;
67
+ font-weight: 700;
68
+ color: #2D3748;
69
+ }
70
+ .metric-label {
71
+ font-size: 0.9rem;
72
+ color: #A0AEC0;
73
+ font-weight: 600;
74
+ }
75
+
76
+ /* Sidebar "Analyst Notes" */
77
+ .sidebar .st-emotion-cache-16txtl3 {
78
+ font-size: 1.2rem;
79
+ font-weight: 600;
80
+ color: #2D3748;
81
+ }
82
+ .note-entry {
83
+ background-color: #F7FAFC;
84
+ border-left: 4px solid #4299E1;
85
+ padding: 1rem;
86
+ border-radius: 4px;
87
+ margin-bottom: 0.75rem;
88
+ }
89
+ .note-title { font-weight: 600; color: #2C5282; margin-bottom: 0.25rem; }
90
+ .note-content { font-size: 0.85rem; color: #4A5568; }
91
+
92
+ /* Alerts Styling */
93
+ .alert-card {
94
+ padding: 1rem;
95
+ border-radius: 6px;
96
+ margin-bottom: 0.8rem;
97
+ border-left: 5px solid #CBD5E0;
98
+ background-color: #F7FAFC;
99
+ }
100
+ .alert-market { border-left-color: #E53E3E; background-color: #FFF5F5; } /* Red for Market */
101
+ .alert-news { border-left-color: #3182CE; background-color: #EBF8FF; } /* Blue for News */
102
+ .alert-header { display: flex; justify-content: space-between; font-size: 0.85rem; color: #718096; margin-bottom: 0.5rem; }
103
+ .alert-body { font-weight: 600; color: #2D3748; }
104
+
105
+ </style>
106
+ """, unsafe_allow_html=True)
107
+
108
+ # --- Helper Functions & State ---
109
+ @st.cache_data(ttl=60)
110
+ def check_server_status():
111
+ urls = {"Gateway": "http://127.0.0.1:8000/", "Tavily": "http://127.0.0.1:8001/",
112
+ "Alpha Vantage": "http://127.0.0.1:8002/", "Private DB": "http://127.0.0.1:8003/"}
113
+ statuses = {}
114
+ with httpx.Client(timeout=2.0) as client:
115
+ for name, url in urls.items():
116
+ try:
117
+ response = client.get(url)
118
+ statuses[name] = "✅ Online" if response.status_code == 200 else "⚠️ Error"
119
+ except:
120
+ statuses[name] = "❌ Offline"
121
+ return statuses
122
+
123
+ def load_watchlist():
124
+ if not os.path.exists(WATCHLIST_FILE): return []
125
+ try:
126
+ with open(WATCHLIST_FILE, 'r') as f: return json.load(f)
127
+ except: return []
128
+
129
+ def save_watchlist(watchlist):
130
+ with open(WATCHLIST_FILE, 'w') as f: json.dump(watchlist, f)
131
+
132
+ def load_alerts():
133
+ if not os.path.exists(ALERTS_FILE): return []
134
+ try:
135
+ with open(ALERTS_FILE, 'r') as f: return json.load(f)
136
+ except: return []
137
+
138
+ if 'final_state' not in st.session_state:
139
+ st.session_state.final_state = None
140
+
141
+ # --- UI Rendering ---
142
+
143
+ # Header
144
+ st.markdown('<h1 class="main-header">Aegis Digital Briefing Room</h1>', unsafe_allow_html=True)
145
+ st.markdown('<p class="subtitle">Automated Intelligence Reports for Modern Finance</p>', unsafe_allow_html=True)
146
+
147
+ # --- SIDEBAR: Watchlist & Notes ---
148
+ sidebar = st.sidebar
149
+ sidebar.title("🛡️ Command Center")
150
+
151
+ # 1. Watchlist Manager
152
+ sidebar.subheader("Active Watchlist")
153
+ watchlist = load_watchlist()
154
+ new_symbol = sidebar.text_input("Add Symbol:", placeholder="e.g. MSFT").upper()
155
+ if sidebar.button("Add to Watchlist"):
156
+ if new_symbol and new_symbol not in watchlist:
157
+ watchlist.append(new_symbol)
158
+ save_watchlist(watchlist)
159
+ st.rerun()
160
+
161
+ symbol_to_remove = sidebar.selectbox("Remove Symbol:", ["Select..."] + watchlist)
162
+ if symbol_to_remove != "Select..." and sidebar.button("Remove"):
163
+ watchlist.remove(symbol_to_remove)
164
+ save_watchlist(watchlist)
165
+ st.rerun()
166
+
167
+ sidebar.markdown("---")
168
+
169
+ # 2. Analyst Notes
170
+ sidebar.title("👨‍💼 Analyst's Live Notes")
171
+ notes_placeholder = sidebar.empty()
172
+ notes_placeholder.info("Awaiting new directive...")
173
+
174
+ # --- MAIN CONTENT ---
175
+ main_col, alerts_col = st.columns([3, 1])
176
+
177
+ with main_col:
178
+ # Main container for Research
179
+ main_container = st.container(border=True)
180
+
181
+ # Input Form
182
+ with main_container:
183
+ st.subheader("🚀 Launch On-Demand Analysis")
184
+ with st.form("research_form"):
185
+ task_input = st.text_input("", placeholder="Enter your directive, e.g., 'Analyze market reaction to the latest Apple ($AAPL) product launch'", label_visibility="collapsed")
186
+ submitted = st.form_submit_button("Generate Briefing", use_container_width=True)
187
+
188
+ # --- Main Logic ---
189
+ if submitted and task_input:
190
+ server_statuses = check_server_status()
191
+ if not all(s == "✅ Online" for s in server_statuses.values()):
192
+ main_container.error("Analysis cannot proceed. One or more backend services are offline. Please check the status.")
193
+ else:
194
+ # main_container.empty() # Don't clear, just show results below
195
+
196
+ final_state_result = {}
197
+ analyst_notes = []
198
+
199
+ try:
200
+ with st.spinner("Your AI Analyst is compiling the briefing... This may take a moment."):
201
+ for event in SentinelOrchestratorV3.stream({"task": task_input}):
202
+ node_name = list(event.keys())[0]
203
+ final_state_result.update(event[node_name])
204
+
205
+ # --- Generate and Display Live Analyst Notes ---
206
+ note = ""
207
+ if node_name == "extract_symbol":
208
+ note = f"Identified target entity: **{event[node_name].get('symbol', 'N/A')}**"
209
+ elif node_name == "web_researcher":
210
+ note = "Sourced initial open-source intelligence from the web."
211
+ elif node_name == "market_data_analyst":
212
+ note = "Retrieved latest intraday market performance data."
213
+ elif node_name == "data_analyzer":
214
+ note = "Commenced deep-dive quantitative analysis of time-series data."
215
+ elif node_name == "report_synthesizer":
216
+ note = "Synthesizing all findings into the final executive briefing."
217
+
218
+ if note:
219
+ analyst_notes.append(f'<div class="note-entry"><div class="note-title">{node_name.replace("_", " ").title()}</div><div class="note-content">{note}</div></div>')
220
+ notes_placeholder.markdown("".join(analyst_notes), unsafe_allow_html=True)
221
+ time.sleep(0.5)
222
+
223
+ # --- Display the Final Briefing ---
224
+ st.session_state.final_state = final_state_result
225
+ final_state = st.session_state.final_state
226
+ symbol = final_state.get("symbol", "N/A")
227
+
228
+ # HEADLINE
229
+ st.markdown(f"## Briefing: {symbol} - {datetime.now().strftime('%B %d, %Y')}")
230
+ st.markdown("---")
231
+
232
+ # KEY METRICS WIDGET
233
+ st.subheader("Key Performance Indicators")
234
+ df = final_state.get("analysis_results", {}).get("dataframe")
235
+ if df is not None and not df.empty:
236
+ m_col1, m_col2, m_col3, m_col4 = st.columns(4)
237
+ with m_col1:
238
+ st.markdown(f'<div class="metric-card"><div class="metric-value">${df["close"].iloc[-1]:.2f}</div><div class="metric-label">Latest Close Price</div></div>', unsafe_allow_html=True)
239
+ with m_col2:
240
+ st.markdown(f'<div class="metric-card"><div class="metric-value">{df["volume"].sum()/1e6:.2f}M</div><div class="metric-label">Total Volume</div></div>', unsafe_allow_html=True)
241
+ with m_col3:
242
+ st.markdown(f'<div class="metric-card"><div class="metric-value">${df["high"].max():.2f}</div><div class="metric-label">Intraday High</div></div>', unsafe_allow_html=True)
243
+ with m_col4:
244
+ st.markdown(f'<div class="metric-card"><div class="metric-value">${df["low"].min():.2f}</div><div class="metric-label">Intraday Low</div></div>', unsafe_allow_html=True)
245
+ else:
246
+ st.info("Quantitative market data was not applicable for this briefing.")
247
+
248
+ st.markdown("<br>", unsafe_allow_html=True)
249
+
250
+ # MAIN BRIEFING (REPORT + CHARTS)
251
+ brief_col1, brief_col2 = st.columns([7, 5]) # 70/50 split
252
+ with brief_col1:
253
+ st.subheader("Executive Summary & Analysis")
254
+ report_html = final_state.get("final_report", "No report generated.").replace("\n", "<br>")
255
+ st.markdown(f'<div class="card" style="height: 100%;">{report_html}</div>', unsafe_allow_html=True)
256
+
257
+ with brief_col2:
258
+ st.subheader("Visual Data Debrief")
259
+ charts = final_state.get("analysis_results", {}).get("charts", [])
260
+ if charts:
261
+ for chart in charts:
262
+ st.plotly_chart(chart, use_container_width=True)
263
+ else:
264
+ st.markdown('<div class="card" style="height: 100%;"><p>No visualizations were generated for this briefing.</p></div>', unsafe_allow_html=True)
265
+
266
+ # EVIDENCE LOG
267
+ with st.expander("Show Evidence Log & Methodology"):
268
+ st.markdown("#### Open Source Intelligence (Web Research)")
269
+ st.json(final_state.get('web_research_results', '{}'))
270
+ st.markdown("#### Deep-Dive Analysis Insights")
271
+ st.text(final_state.get("analysis_results", {}).get("insights", "No insights."))
272
+
273
+ if st.button("Start New Briefing"):
274
+ st.session_state.final_state = None
275
+ st.rerun()
276
+
277
+ except Exception as e:
278
+ st.error(f"An error occurred: {e}")
279
+
280
+ # --- LIVE ALERTS FEED ---
281
+ with alerts_col:
282
+ st.subheader("🚨 Live Alerts")
283
+ st.caption("Real-time monitoring feed")
284
+
285
+ alerts_container = st.container(height=600)
286
+
287
+ # Auto-refresh logic for alerts
288
+ if 'last_refresh' not in st.session_state:
289
+ st.session_state.last_refresh = time.time()
290
+
291
+ # Refresh every 10 seconds
292
+ if time.time() - st.session_state.last_refresh > 10:
293
+ st.session_state.last_refresh = time.time()
294
+ st.rerun()
295
+
296
+ alerts = load_alerts()
297
+ if not alerts:
298
+ alerts_container.info("No active alerts.")
299
+ else:
300
+ for alert in alerts:
301
+ alert_type = alert.get("type", "INFO")
302
+ css_class = "alert-market" if alert_type == "MARKET" else "alert-news" if alert_type == "NEWS" else ""
303
+ icon = "📉" if alert_type == "MARKET" else "📰"
304
+
305
+ timestamp = datetime.fromisoformat(alert.get("timestamp", datetime.now().isoformat())).strftime("%H:%M")
306
+
307
+ html = f"""
308
+ <div class="alert-card {css_class}">
309
+ <div class="alert-header">
310
+ <span>{icon} {alert.get("symbol")}</span>
311
+ <span>{timestamp}</span>
312
+ </div>
313
+ <div class="alert-body">
314
+ {alert.get("message")}
315
+ </div>
316
+ </div>
317
+ """
318
+ alerts_container.markdown(html, unsafe_allow_html=True)
create_dummy_db.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # create_dummy_db.py
2
+ import sqlite3
3
+
4
+ # Connect to the SQLite database (this will create the file if it doesn't exist)
5
+ conn = sqlite3.connect('portfolio.db')
6
+ cursor = conn.cursor()
7
+
8
+ # --- Create the holdings table ---
9
+ cursor.execute('''
10
+ CREATE TABLE IF NOT EXISTS holdings (
11
+ id INTEGER PRIMARY KEY,
12
+ symbol TEXT NOT NULL UNIQUE,
13
+ shares INTEGER NOT NULL,
14
+ average_cost REAL NOT NULL
15
+ )
16
+ ''')
17
+ print("Table 'holdings' created successfully.")
18
+
19
+ # --- Insert some sample data ---
20
+ # Using INSERT OR IGNORE to prevent errors if you run the script multiple times
21
+ holdings_data = [
22
+ ('NVDA', 1500, 250.75),
23
+ ('AAPL', 5000, 180.20),
24
+ ('IBM', 2500, 155.45),
25
+ ('TSLA', 1000, 220.90)
26
+ ]
27
+
28
+ cursor.executemany('''
29
+ INSERT OR IGNORE INTO holdings (symbol, shares, average_cost) VALUES (?, ?, ?)
30
+ ''', holdings_data)
31
+ print(f"{len(holdings_data)} sample holdings inserted.")
32
+
33
+ # --- Commit the changes and close the connection ---
34
+ conn.commit()
35
+ conn.close()
36
+ print("Database 'portfolio.db' is set up and ready.")
debug_gemini.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from dotenv import load_dotenv
3
+ import google.generativeai as genai
4
+
5
+ load_dotenv()
6
+
7
+ api_key = os.getenv("GOOGLE_API_KEY")
8
+ if not api_key:
9
+ print("GOOGLE_API_KEY not found.")
10
+ exit(1)
11
+
12
+ genai.configure(api_key=api_key)
13
+
14
+ print("Listing available models...")
15
+ try:
16
+ for m in genai.list_models():
17
+ if 'generateContent' in m.supported_generation_methods:
18
+ print(f"Name: {m.name}")
19
+ except Exception as e:
20
+ print(f"Error listing models: {e}")
deploy_local.sh ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ # deploy_local.sh - "Poor Man's Deployment"
4
+ # Runs Aegis services in the background using nohup.
5
+ # Useful if Docker is not available or not working.
6
+
7
+ echo "🚀 Starting Aegis in Production Mode (Local)..."
8
+
9
+ # 1. Activate Virtual Environment
10
+ source venv/bin/activate
11
+
12
+ # 2. Kill existing processes on ports
13
+ echo "🧹 Cleaning up ports..."
14
+ lsof -ti:8000,8001,8002,8003,8501 | xargs kill -9 2>/dev/null
15
+
16
+ # 3. Create logs directory
17
+ mkdir -p logs
18
+
19
+ # 4. Start Services in Background
20
+ echo "Starting Gateway..."
21
+ nohup uvicorn mcp_gateway:app --host 0.0.0.0 --port 8000 > logs/gateway.log 2>&1 &
22
+ PID_GATEWAY=$!
23
+ echo "Gateway PID: $PID_GATEWAY"
24
+
25
+ echo "Starting Tavily Service..."
26
+ nohup uvicorn tavily_mcp:app --host 0.0.0.0 --port 8001 > logs/tavily.log 2>&1 &
27
+
28
+ echo "Starting Alpha Vantage Service..."
29
+ nohup uvicorn alphavantage_mcp:app --host 0.0.0.0 --port 8002 > logs/alphavantage.log 2>&1 &
30
+
31
+ echo "Starting Portfolio Service..."
32
+ nohup uvicorn private_mcp:app --host 0.0.0.0 --port 8003 > logs/portfolio.log 2>&1 &
33
+
34
+ echo "Starting Monitor..."
35
+ nohup python monitor.py > logs/monitor.log 2>&1 &
36
+
37
+ echo "Starting Frontend..."
38
+ nohup streamlit run app.py --server.port 8501 --server.address 0.0.0.0 > logs/frontend.log 2>&1 &
39
+
40
+ echo "✅ Deployment Complete!"
41
+ echo "---------------------------------------------------"
42
+ echo "🌐 Frontend: http://localhost:8501"
43
+ echo "📂 Logs are being written to the 'logs/' directory."
44
+ echo "🛑 To stop all services, run: pkill -f 'uvicorn|streamlit|monitor.py'"
45
+ echo "---------------------------------------------------"
deployment_guide.md ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Aegis Deployment Guide
2
+
3
+ This guide explains how to deploy the Aegis Financial Analyst Agent using Docker and Docker Compose.
4
+
5
+ ## Prerequisites
6
+ - **Docker** and **Docker Compose** installed on your machine.
7
+ - An `.env` file with valid API keys (see `.env.example` or your existing `.env`).
8
+
9
+ ## Deployment Steps
10
+
11
+ 1. **Build and Start Services**
12
+ Run the following command in the project root directory:
13
+ ```bash
14
+ docker-compose up --build -d
15
+ ```
16
+ This will:
17
+ - Build the Docker image for the application.
18
+ - Create a network `aegis-net`.
19
+ - Start all services (Gateway, Microservices, Monitor, Frontend) in detached mode.
20
+
21
+ 2. **Verify Deployment**
22
+ - **Frontend**: Access the Streamlit UI at `http://localhost:8501`.
23
+ - **Gateway**: `http://localhost:8000`
24
+ - **Services**:
25
+ - Tavily: `http://localhost:8001`
26
+ - Alpha Vantage: `http://localhost:8002`
27
+ - Portfolio: `http://localhost:8003`
28
+
29
+ 3. **View Logs**
30
+ To see logs for all services:
31
+ ```bash
32
+ docker-compose logs -f
33
+ ```
34
+ To see logs for a specific service (e.g., frontend):
35
+ ```bash
36
+ docker-compose logs -f frontend
37
+ ```
38
+
39
+ 4. **Stop Services**
40
+ To stop and remove containers:
41
+ ```bash
42
+ docker-compose down
43
+ ```
44
+
45
+ ## Environment Variables
46
+ Ensure your `.env` file contains:
47
+
48
+ - `GOOGLE_API_KEY`
49
+ - `TAVILY_API_KEY`
50
+ - `ALPHA_VANTAGE_API_KEY`
51
+
52
+ Docker Compose automatically reads these from the `.env` file in the same directory.
53
+
54
+ ## Alternative Deployment (No Docker)
55
+ If you cannot run Docker, use the local deployment script:
56
+ ```bash
57
+ ./deploy_local.sh
58
+ ```
59
+ This runs all services in the background and saves logs to a `logs/` folder.
60
+
61
+ ## Troubleshooting
62
+ - **"Cannot connect to the Docker daemon"**: This means Docker is not running. Open **Docker Desktop** on your Mac and wait for it to start (the whale icon in the menu bar should stop animating).
63
+ - **Port Conflicts**: Ensure ports 8000-8003 and 8501 are free.
64
+ - **Database Persistence**: The `portfolio.db` file is mounted as a volume, so your internal portfolio data persists across restarts.
docker-compose.yml ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+ services:
4
+ gateway:
5
+ build: .
6
+ command: uvicorn mcp_gateway:app --host 0.0.0.0 --port 8000
7
+ ports:
8
+ - "8000:8000"
9
+ environment:
10
+ - TAVILY_MCP_URL=http://tavily:8001/research
11
+ - ALPHAVANTAGE_MCP_URL=http://alphavantage:8002/market_data
12
+ - PRIVATE_MCP_URL=http://portfolio:8003/portfolio_data
13
+ - TAVILY_API_KEY=${TAVILY_API_KEY}
14
+ - ALPHA_VANTAGE_API_KEY=${ALPHA_VANTAGE_API_KEY}
15
+ - GOOGLE_API_KEY=${GOOGLE_API_KEY}
16
+
17
+ networks:
18
+ - aegis-net
19
+
20
+ tavily:
21
+ build: .
22
+ command: uvicorn tavily_mcp:app --host 0.0.0.0 --port 8001
23
+ ports:
24
+ - "8001:8001"
25
+ environment:
26
+ - TAVILY_API_KEY=${TAVILY_API_KEY}
27
+ networks:
28
+ - aegis-net
29
+
30
+ alphavantage:
31
+ build: .
32
+ command: uvicorn alphavantage_mcp:app --host 0.0.0.0 --port 8002
33
+ ports:
34
+ - "8002:8002"
35
+ environment:
36
+ - ALPHA_VANTAGE_API_KEY=${ALPHA_VANTAGE_API_KEY}
37
+ networks:
38
+ - aegis-net
39
+
40
+ portfolio:
41
+ build: .
42
+ command: uvicorn private_mcp:app --host 0.0.0.0 --port 8003
43
+ ports:
44
+ - "8003:8003"
45
+ volumes:
46
+ - ./portfolio.db:/app/portfolio.db
47
+ networks:
48
+ - aegis-net
49
+
50
+ monitor:
51
+ build: .
52
+ command: python monitor.py
53
+ environment:
54
+ - MCP_GATEWAY_URL=http://gateway:8000/route_agent_request
55
+ - TAVILY_API_KEY=${TAVILY_API_KEY}
56
+ - ALPHA_VANTAGE_API_KEY=${ALPHA_VANTAGE_API_KEY}
57
+ depends_on:
58
+ - gateway
59
+ networks:
60
+ - aegis-net
61
+
62
+ frontend:
63
+ build: .
64
+ command: streamlit run app.py --server.port 8501 --server.address 0.0.0.0
65
+ ports:
66
+ - "8501:8501"
67
+ environment:
68
+ - MCP_GATEWAY_URL=http://gateway:8000/route_agent_request
69
+ - GOOGLE_API_KEY=${GOOGLE_API_KEY}
70
+
71
+ depends_on:
72
+ - gateway
73
+ networks:
74
+ - aegis-net
75
+
76
+ networks:
77
+ aegis-net:
78
+ driver: bridge
linkedin_post.md ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🚀 LinkedIn Post Drafts for Aegis / Sentinel
2
+
3
+ Here are three variations for your LinkedIn post, depending on the angle you want to take.
4
+
5
+ ## Option 1: The "Technical Deep Dive" (Best for Engineering Networks)
6
+ **Headline:** Building a Proactive Financial AI Agent with LangGraph & Microservices 🛡️
7
+
8
+ I just wrapped up building **Aegis**, a "Premium Financial Terminal" powered by AI. It’s not just a chatbot; it’s a fully orchestrated agentic system.
9
+
10
+ **The Tech Stack:**
11
+ * **Brain:** LangGraph for stateful orchestration (Google Gemini).
12
+ * **Architecture:** Microservices pattern using the **Model Context Protocol (MCP)**.
13
+ * **Real-Time:** A background monitor that watches my portfolio and pushes alerts.
14
+ * **Frontend:** Streamlit for that Bloomberg Terminal vibe.
15
+
16
+ **How it works:**
17
+ The "Orchestrator" breaks down natural language directives (e.g., "Analyze TSLA and check my exposure"). It routes tasks through a FastAPI Gateway to specialized agents: a **Web Researcher** (Tavily), a **Market Analyst** (Alpha Vantage), and a **Portfolio Manager** (Local DB).
18
+
19
+ It’s been a great journey learning how to decouple AI tools from the core logic.
20
+
21
+ #AI #LangGraph #Python #Microservices #FinTech #LLM #OpenSource
22
+
23
+ ---
24
+
25
+ ## Option 2: The "Product Showcase" (Best for General Audience)
26
+ **Headline:** Meet Sentinel: My Personal AI Financial Analyst ⚡
27
+
28
+ Tired of switching between news sites, stock charts, and my brokerage app, I decided to build my own solution.
29
+
30
+ Introducing **Sentinel** (Project Aegis) – an AI agent that acts as a proactive financial analyst.
31
+
32
+ **What it does:**
33
+ ✅ **Deep Dives:** I ask "Why is Apple down?", and it reads the news, checks the charts, and writes a report.
34
+ ✅ **24/7 Monitoring:** It watches my watchlist and alerts me to price spikes or breaking news.
35
+ ✅ **Portfolio Context:** It knows what I own, so its advice is personalized.
36
+
37
+ The power of Agentic AI is that it doesn't just talk; it *does*. It plans, researches, and synthesizes information faster than I ever could.
38
+
39
+ #ArtificialIntelligence #FinTech #Productivity #Coding #Streamlit
40
+
41
+ ---
42
+
43
+ ## Option 3: The "Learning Journey" (Best for Engagement)
44
+ **Headline:** From "Chatbot" to "Agentic System" – My latest build 🧠
45
+
46
+ I spent the last few days building **Aegis**, and it completely changed how I think about AI applications.
47
+
48
+ I started with a simple script, but realized that for complex tasks like financial analysis, a single LLM call isn't enough. You need **Agents**.
49
+
50
+ **Key Lessons Learned:**
51
+ 1. **State Management is King:** Using LangGraph to pass context between a "Researcher" and a "Data Analyst" is a game changer.
52
+ 2. **Decoupling Matters:** I built a "Gateway" so I can swap out my News provider without breaking the whole app.
53
+ 3. **Latency vs. Accuracy:** Orchestrating multiple AI calls takes time, but the depth of insight is worth the wait.
54
+
55
+ Check out the architecture in the comments! 👇
56
+
57
+ What are you building with Agents right now?
58
+
59
+ #BuildInPublic #AI #Learning #SoftwareEngineering #TechTrends
logo_helper.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Logo as base64 encoded string
2
+ # This avoids binary file issues with Hugging Face Spaces
3
+
4
+ def get_logo_base64():
5
+ """Returns the Sentinel logo as a base64 encoded string"""
6
+ # If logo file exists, read it
7
+ try:
8
+ import base64
9
+ with open("assets/logo.png", "rb") as f:
10
+ return base64.b64encode(f.read()).decode()
11
+ except:
12
+ # Fallback: return empty string if file not found
13
+ return ""
main.py ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import subprocess
2
+ import sys
3
+ import os
4
+ import time
5
+ import signal
6
+
7
+ def cleanup(signum, frame):
8
+ print("Stopping services...")
9
+ # Add cleanup logic here if needed
10
+ sys.exit(0)
11
+
12
+ signal.signal(signal.SIGINT, cleanup)
13
+ signal.signal(signal.SIGTERM, cleanup)
14
+
15
+ def main():
16
+ print("🚀 Starting Sentinel Monolith...")
17
+
18
+ # 1. Start the MCP Gateway (which now includes all microservices)
19
+ # running on port 8000
20
+ gateway_cmd = [sys.executable, "mcp_gateway.py"]
21
+ gateway_process = subprocess.Popen(gateway_cmd, cwd=os.getcwd())
22
+ print(f"✅ Gateway started (PID: {gateway_process.pid})")
23
+
24
+ # 2. Start the Monitor (runs in background loop)
25
+ # Using the same interpreter
26
+ monitor_cmd = [sys.executable, "monitor.py"]
27
+ monitor_process = subprocess.Popen(monitor_cmd, cwd=os.getcwd())
28
+ print(f"✅ Monitor started (PID: {monitor_process.pid})")
29
+
30
+ # Give backend a moment to initialize
31
+ time.sleep(5)
32
+
33
+ # 3. Start Streamlit (Frontend)
34
+ # This commands blocks until Streamlit exits
35
+ print("✅ Starting Streamlit on port 7860...")
36
+ streamlit_cmd = [
37
+ "streamlit", "run", "app.py",
38
+ "--server.port", "7860",
39
+ "--server.address", "0.0.0.0",
40
+ "--server.headless", "true",
41
+ "--browser.serverAddress", "0.0.0.0",
42
+ "--server.enableCORS", "false",
43
+ "--server.enableXsrfProtection", "false"
44
+ ]
45
+
46
+ # We use subprocess.run for the foreground process
47
+ subprocess.run(streamlit_cmd, check=False)
48
+
49
+ # Cleanup when streamlit exits
50
+ gateway_process.terminate()
51
+ monitor_process.terminate()
52
+
53
+ if __name__ == "__main__":
54
+ main()
mcp_gateway.py ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # mcp_gateway.py
2
+ from fastapi import FastAPI, HTTPException, Request
3
+ from fastapi.responses import JSONResponse
4
+ import uvicorn
5
+ import httpx
6
+ import logging
7
+ import os
8
+ from dotenv import load_dotenv
9
+
10
+ load_dotenv()
11
+
12
+ # --- Logging Setup ---
13
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
14
+ logger = logging.getLogger("MCP_Gateway")
15
+
16
+ # --- Import Microservices for Consolidation ---
17
+ try:
18
+ from tavily_mcp import app as tavily_app
19
+ from alphavantage_mcp import app as alphavantage_app
20
+ from private_mcp import app as private_app
21
+ logger.info("Successfully imported microservices for consolidation.")
22
+ except ImportError as e:
23
+ logger.critical(f"Failed to import microservices: {e}")
24
+ raise
25
+
26
+ # --- Configuration (Updated for Monolithic Mode) ---
27
+ # Default to internal mounted paths on the same port (8000)
28
+ TAVILY_MCP_URL = os.getenv("TAVILY_MCP_URL", "http://127.0.0.1:8000/tavily/research")
29
+ ALPHAVANTAGE_MCP_URL = os.getenv("ALPHAVANTAGE_MCP_URL", "http://127.0.0.1:8000/alphavantage/market_data")
30
+ PRIVATE_MCP_URL = os.getenv("PRIVATE_MCP_URL", "http://127.0.0.1:8000/private/portfolio_data")
31
+
32
+ # --- FastAPI App ---
33
+ app = FastAPI(title="Aegis MCP Gateway (Monolith)")
34
+
35
+ # --- Mount Microservices ---
36
+ app.mount("/tavily", tavily_app)
37
+ app.mount("/alphavantage", alphavantage_app)
38
+ app.mount("/private", private_app)
39
+
40
+ client = httpx.AsyncClient()
41
+
42
+ @app.middleware("http")
43
+ async def audit_log_middleware(request: Request, call_next):
44
+ # Skip logging for internal sub-app calls to reduce noise if needed,
45
+ # but strictly speaking this middleware triggers for the parent app.
46
+ # Requests to mounted apps might bypass this or trigger it depending on path matching.
47
+ logger.info(f"Request received: {request.method} {request.url}")
48
+ response = await call_next(request)
49
+ return response
50
+
51
+ @app.post("/route_agent_request")
52
+ async def route_agent_request(request_data: dict):
53
+ target_service = request_data.get("target_service")
54
+ payload = request_data.get("payload", {})
55
+
56
+ logger.info(f"Routing request for target service: {target_service}")
57
+
58
+ url_map = {
59
+ "tavily_research": TAVILY_MCP_URL,
60
+ "alpha_vantage_market_data": ALPHAVANTAGE_MCP_URL,
61
+ "internal_portfolio_data": PRIVATE_MCP_URL,
62
+ }
63
+
64
+ target_url = url_map.get(target_service)
65
+
66
+ if not target_url:
67
+ logger.error(f"Invalid target service specified: {target_service}")
68
+ raise HTTPException(status_code=400, detail=f"Invalid target service: {target_service}")
69
+
70
+ try:
71
+ # Self-referential call (Gateway -> Mounted App on same server)
72
+ # We must ensure we don't block. HTTPX AsyncClient handles this well.
73
+ response = await client.post(target_url, json=payload, timeout=180.0)
74
+ response.raise_for_status()
75
+ return JSONResponse(content=response.json(), status_code=response.status_code)
76
+
77
+ except httpx.HTTPStatusError as e:
78
+ logger.error(f"Error from microservice {target_service}: {e.response.text}")
79
+ raise HTTPException(status_code=e.response.status_code, detail=e.response.json())
80
+ except httpx.RequestError as e:
81
+ logger.error(f"Could not connect to microservice {target_service}: {e}")
82
+ raise HTTPException(status_code=503, detail=f"Service '{target_service}' is unavailable.")
83
+ except Exception as e:
84
+ logger.critical(f"An unexpected error occurred during routing: {e}")
85
+ raise HTTPException(status_code=500, detail="Internal server error in MCP Gateway.")
86
+
87
+ @app.get("/")
88
+ def read_root():
89
+ return {"message": "Aegis MCP Gateway (Monolithic) is operational."}
90
+
91
+ if __name__ == "__main__":
92
+ uvicorn.run(app, host="127.0.0.1", port=8000)
93
+
monitor.py ADDED
@@ -0,0 +1,179 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # monitor.py
2
+ import time
3
+ import json
4
+ import os
5
+ import sys
6
+ import logging
7
+ from datetime import datetime
8
+ from agents.tool_calling_agents import MarketDataAgent, WebResearchAgent
9
+
10
+ # --- Configuration ---
11
+ WATCHLIST_FILE = "watchlist.json"
12
+ ALERTS_FILE = "alerts.json"
13
+ CHECK_INTERVAL = 10 # 10 seconds (Real-time feel)
14
+ PRICE_ALERT_THRESHOLD = 0.5 # More sensitive alerts
15
+
16
+ # --- Logging Setup ---
17
+ logging.basicConfig(
18
+ level=logging.INFO,
19
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
20
+ handlers=[
21
+ logging.StreamHandler(sys.stdout)
22
+ ]
23
+ )
24
+ logger = logging.getLogger("Aegis_Monitor")
25
+
26
+ # --- Initialize Agents ---
27
+ market_agent = MarketDataAgent()
28
+ web_agent = WebResearchAgent()
29
+
30
+ def load_watchlist():
31
+ if not os.path.exists(WATCHLIST_FILE):
32
+ return []
33
+ try:
34
+ with open(WATCHLIST_FILE, 'r') as f:
35
+ return json.load(f)
36
+ except Exception as e:
37
+ logger.error(f"Error loading watchlist: {e}")
38
+ return []
39
+
40
+ def save_alert(alert):
41
+ alerts = []
42
+ if os.path.exists(ALERTS_FILE):
43
+ try:
44
+ with open(ALERTS_FILE, 'r') as f:
45
+ alerts = json.load(f)
46
+ except:
47
+ pass
48
+
49
+ # Prepend new alert
50
+ alerts.insert(0, alert)
51
+ # Keep only last 100 alerts (increased from 50)
52
+ alerts = alerts[:100]
53
+
54
+ with open(ALERTS_FILE, 'w') as f:
55
+ json.dump(alerts, f, indent=2)
56
+
57
+ def check_market_data(symbol):
58
+ try:
59
+ logger.info(f"Checking market data for {symbol}...")
60
+ # Get compact intraday data (Corrected method call)
61
+ result = market_agent.get_market_data(symbol=symbol, time_range="INTRADAY")
62
+
63
+ if result.get("status") != "success":
64
+ logger.warning(f"Failed to get market data for {symbol}")
65
+ return None
66
+
67
+ data = result.get("data", {})
68
+ if not data:
69
+ return None
70
+
71
+ # Get latest and 15-minute-ago data points to calculate change
72
+ timestamps = sorted(list(data.keys()), reverse=True)
73
+ if len(timestamps) < 4: # Need at least 4 points (15 mins = 3 intervals)
74
+ return None
75
+
76
+ latest = data[timestamps[0]]
77
+ baseline = data[timestamps[min(3, len(timestamps)-1)]] # 15 mins ago
78
+
79
+ close_latest = float(latest.get("4. close", 0))
80
+ close_baseline = float(baseline.get("4. close", 0))
81
+
82
+ if close_baseline == 0:
83
+ return None
84
+
85
+ pct_change = ((close_latest - close_baseline) / close_baseline) * 100
86
+
87
+ return {
88
+ "price": close_latest,
89
+ "change": pct_change,
90
+ "timestamp": timestamps[0]
91
+ }
92
+ except Exception as e:
93
+ logger.error(f"Error checking market data for {symbol}: {e}")
94
+ return None
95
+
96
+ def check_news(symbol):
97
+ try:
98
+ logger.info(f"Checking news for {symbol}...")
99
+ query = f"breaking news {symbol} stock today"
100
+ result = web_agent.research(queries=[query], search_depth="basic")
101
+
102
+ if result.get("status") != "success":
103
+ return None
104
+
105
+ # Just return the first result title for now as a "headline"
106
+ data = result.get("data", [])
107
+ if data and data[0].get("results"):
108
+ first_hit = data[0]["results"][0]
109
+ return {
110
+ "title": first_hit.get("title"),
111
+ "url": first_hit.get("url"),
112
+ "content": first_hit.get("content")[:200] + "..."
113
+ }
114
+ return None
115
+ except Exception as e:
116
+ logger.error(f"Error checking news for {symbol}: {e}")
117
+ return None
118
+
119
+ def run_monitor_loop():
120
+ logger.info("--- 🛡️ Aegis Proactive Monitor Started ---")
121
+ logger.info(f"Monitoring watchlist every {CHECK_INTERVAL} seconds ({CHECK_INTERVAL/60:.0f} minutes).")
122
+ logger.info(f"Price alert threshold: {PRICE_ALERT_THRESHOLD}%")
123
+
124
+ while True:
125
+ watchlist = load_watchlist()
126
+ if not watchlist:
127
+ logger.info("Watchlist is empty. Waiting...")
128
+
129
+ for symbol in watchlist:
130
+ try:
131
+ # 1. Market Check
132
+ market_info = check_market_data(symbol)
133
+ if market_info:
134
+ # Alert Logic: Price moved more than threshold
135
+ if abs(market_info['change']) > PRICE_ALERT_THRESHOLD:
136
+ direction = "📈 UP" if market_info['change'] > 0 else "📉 DOWN"
137
+ alert_msg = f"{direction} ALERT: {symbol} moved {market_info['change']:+.2f}% to ${market_info['price']:.2f}"
138
+ logger.info(alert_msg)
139
+
140
+ save_alert({
141
+ "timestamp": datetime.now().isoformat(),
142
+ "type": "MARKET",
143
+ "symbol": symbol,
144
+ "message": alert_msg,
145
+ "details": market_info
146
+ })
147
+
148
+ # 2. News Check (Simplified: Just log latest headline)
149
+ news_info = check_news(symbol)
150
+ if news_info:
151
+ # Check if this is "significant" news based on keywords
152
+ keywords = [
153
+ "acquisition", "merger", "earnings", "crash", "surge", "plunge",
154
+ "fda", "lawsuit", "sec", "filing", "8-k", "10-k", "insider",
155
+ "partnership", "deal", "bankruptcy", "recall", "investigation",
156
+ "upgrade", "downgrade", "target", "buyback", "dividend"
157
+ ]
158
+ if any(k in news_info['title'].lower() for k in keywords):
159
+ alert_msg = f"📰 NEWS ALERT: {symbol} - {news_info['title']}"
160
+ logger.info(alert_msg)
161
+
162
+ save_alert({
163
+ "timestamp": datetime.now().isoformat(),
164
+ "type": "NEWS",
165
+ "symbol": symbol,
166
+ "message": alert_msg,
167
+ "details": news_info
168
+ })
169
+
170
+ except Exception as e:
171
+ logger.error(f"Error processing {symbol}: {e}")
172
+
173
+ logger.info(f"Cycle complete. Sleeping for {CHECK_INTERVAL}s...")
174
+ time.sleep(CHECK_INTERVAL)
175
+
176
+ if __name__ == "__main__":
177
+ # Ensure we can import agents
178
+ sys.path.append(os.path.abspath(os.path.dirname(__file__)))
179
+ run_monitor_loop()
packages.txt ADDED
File without changes
private_mcp.py ADDED
@@ -0,0 +1,175 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # private_mcp.py
2
+ from fastapi import FastAPI, HTTPException
3
+ import uvicorn
4
+ import sqlite3
5
+ import logging
6
+ from langchain_core.prompts import ChatPromptTemplate
7
+ from langchain_core.output_parsers import StrOutputParser
8
+ from langchain_ollama import ChatOllama
9
+
10
+ # --- Logging Setup ---
11
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
12
+ logger = logging.getLogger("Private_MCP_Server")
13
+
14
+ # --- Database Configuration ---
15
+ DB_FILE = "portfolio.db"
16
+
17
+ # --- LLM Configuration (Local Llama 3) ---
18
+ # This connects to the Ollama application running on your machine.
19
+ # Make sure Ollama and the llama3 model are running.
20
+ llm = ChatOllama(model="llama3", temperature=0)
21
+
22
+ # --- Text-to-SQL Prompt Engineering ---
23
+ # This prompt is carefully designed to make Llama 3 generate ONLY safe SQL queries.
24
+ text_to_sql_prompt = ChatPromptTemplate.from_messages([
25
+ ("system",
26
+ """You are a Text-to-SQL assistant. Convert the question to a read-only SQLite query for the 'holdings' table.
27
+ Schema: symbol (TEXT), shares (INTEGER), average_cost (REAL).
28
+ RULES:
29
+ 1. SELECT only. No INSERT/UPDATE/DELETE.
30
+ 2. Output ONLY the SQL query. No markdown.
31
+ """),
32
+ ("human", "Question: {question}")
33
+ ])
34
+
35
+ # Create the LangChain chain for Text-to-SQL
36
+ sql_generation_chain = text_to_sql_prompt | llm | StrOutputParser()
37
+
38
+ # --- FastAPI App ---
39
+ app = FastAPI(title="Aegis Private MCP Server")
40
+
41
+ @app.on_event("startup")
42
+ async def startup_db():
43
+ """Initialize the database with dummy data if it doesn't exist."""
44
+ try:
45
+ with sqlite3.connect(DB_FILE) as conn:
46
+ cursor = conn.cursor()
47
+ cursor.execute("""
48
+ CREATE TABLE IF NOT EXISTS holdings (
49
+ symbol TEXT PRIMARY KEY,
50
+ shares INTEGER,
51
+ average_cost REAL
52
+ )
53
+ """)
54
+
55
+ # Check if data exists
56
+ cursor.execute("SELECT count(*) FROM holdings")
57
+ if cursor.fetchone()[0] == 0:
58
+ logger.info("Populating database with diverse dummy data...")
59
+ # Expanded list of companies across sectors
60
+ dummy_data = [
61
+ # Tech
62
+ ('AAPL', 5000, 180.20), ('MSFT', 3000, 350.50), ('GOOGL', 1500, 140.10), ('NVDA', 800, 450.00), ('AMD', 2000, 110.30),
63
+ ('INTC', 4000, 35.40), ('CRM', 1200, 220.10), ('ADBE', 600, 550.20), ('ORCL', 2500, 115.50), ('CSCO', 3500, 52.10),
64
+ # Finance
65
+ ('JPM', 2000, 150.40), ('BAC', 5000, 32.10), ('GS', 500, 340.50), ('V', 1000, 240.20), ('MA', 800, 380.10),
66
+ # Retail & Consumer
67
+ ('WMT', 1500, 160.30), ('TGT', 1000, 130.50), ('COST', 400, 550.10), ('KO', 3000, 58.20), ('PEP', 2500, 170.40),
68
+ ('PG', 2000, 150.10), ('NKE', 1200, 105.30), ('SBUX', 1800, 95.40),
69
+ # Healthcare
70
+ ('JNJ', 2500, 160.20), ('PFE', 4000, 35.10), ('UNH', 600, 480.50), ('LLY', 400, 580.10), ('MRK', 2000, 110.20),
71
+ # Energy & Industrial
72
+ ('XOM', 3000, 105.40), ('CVX', 2000, 150.20), ('GE', 1500, 110.50), ('CAT', 800, 280.10), ('BA', 500, 210.30),
73
+ # Auto
74
+ ('TSLA', 1000, 220.90), ('F', 5000, 12.10), ('GM', 4000, 35.40)
75
+ ]
76
+ cursor.executemany("INSERT INTO holdings (symbol, shares, average_cost) VALUES (?, ?, ?)", dummy_data)
77
+ conn.commit()
78
+ logger.info("Database populated successfully.")
79
+ else:
80
+ logger.info("Database already contains data.")
81
+ except Exception as e:
82
+ logger.error(f"Failed to initialize database: {e}")
83
+
84
+
85
+ def execute_safe_query(query: str, params=None):
86
+ """
87
+ Executes a SQL query after a basic safety check.
88
+ This is a critical security function.
89
+ """
90
+ # SECURITY CHECK: Ensure the query is read-only.
91
+ if not query.strip().upper().startswith("SELECT"):
92
+ logger.error(f"SECURITY VIOLATION: Attempted to execute non-SELECT query: {query}")
93
+ raise HTTPException(status_code=403, detail="Forbidden: Only SELECT queries are allowed.")
94
+
95
+ try:
96
+ with sqlite3.connect(DB_FILE) as conn:
97
+ conn.row_factory = sqlite3.Row # Makes results dict-like
98
+ cursor = conn.cursor()
99
+ if params:
100
+ cursor.execute(query, params)
101
+ else:
102
+ cursor.execute(query)
103
+
104
+ results = [dict(row) for row in cursor.fetchall()]
105
+ # Sanitize results: Replace None with 0 (common for SUM on empty set)
106
+ for row in results:
107
+ for key, value in row.items():
108
+ if value is None:
109
+ row[key] = 0
110
+ return results
111
+ except sqlite3.Error as e:
112
+ logger.error(f"Database error executing query '{query}': {e}")
113
+ raise HTTPException(status_code=500, detail=f"Database query failed: {e}")
114
+
115
+ @app.post("/portfolio_data")
116
+ async def get_portfolio_data(payload: dict):
117
+ """
118
+ Takes a natural language question, converts it to SQL using Llama 3,
119
+ and executes it against the internal portfolio database.
120
+ """
121
+ question = payload.get("question")
122
+ if not question:
123
+ raise HTTPException(status_code=400, detail="'question' is a required field.")
124
+
125
+ logger.info(f"Received portfolio data question: '{question}'")
126
+
127
+ try:
128
+ # Step 1: Generate the SQL query using the local LLM
129
+ try:
130
+ generated_sql = await sql_generation_chain.ainvoke({"question": question})
131
+ logger.info(f"Llama 3 generated SQL: {generated_sql}")
132
+ except Exception as llm_error:
133
+ logger.warning(f"LLM generation failed (likely Ollama offline): {llm_error}. Using fallback logic.")
134
+ # Fallback Logic: Dynamic symbol extraction
135
+ import re
136
+ q_upper = question.upper()
137
+ # Look for common ticker patterns (1-5 uppercase letters)
138
+ matches = re.findall(r'\b[A-Z]{1,5}\b', q_upper)
139
+
140
+ found_symbol = None
141
+ ignored_words = ["WHAT", "IS", "THE", "TO", "OF", "FOR", "IN", "AND", "OR", "SHOW", "ME", "DATA", "STOCK", "PRICE", "DO", "WE", "OWN", "HAVE", "ANY", "EXPOSURE", "CURRENT"]
142
+
143
+ for match in matches:
144
+ if match not in ignored_words:
145
+ found_symbol = match
146
+ break
147
+
148
+ if found_symbol:
149
+ generated_sql = f"SELECT * FROM holdings WHERE symbol='{found_symbol}'"
150
+ else:
151
+ generated_sql = "SELECT * FROM holdings" # Default to showing all
152
+ logger.info(f"Fallback SQL generated: {generated_sql}")
153
+
154
+ # Step 2: Execute the query using our secure function
155
+ results = execute_safe_query(generated_sql)
156
+ logger.info(f"Successfully executed query and found {len(results)} records.")
157
+
158
+ return {"status": "success", "question": question, "generated_sql": generated_sql, "data": results}
159
+
160
+ except HTTPException as http_exc:
161
+ # Re-raise HTTP exceptions from our secure executor
162
+ raise http_exc
163
+ except Exception as e:
164
+ logger.critical(f"An unexpected error occurred in the portfolio data endpoint: {e}")
165
+ # Don't crash the client, return an empty success with error note
166
+ return {"status": "error", "message": str(e), "data": []}
167
+
168
+ @app.get("/")
169
+ def read_root():
170
+ return {"message": "Aegis Private MCP Server is operational."}
171
+
172
+ # --- Main Execution ---
173
+ if __name__ == "__main__":
174
+ # This server runs on port 8003
175
+ uvicorn.run(app, host="127.0.0.1", port=8003)
requirements.txt ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ streamlit
2
+ langchain
3
+ langchain-core
4
+ langgraph
5
+ pydantic<3,>=2
6
+ pandas
7
+ plotly
8
+ python-dotenv
9
+ httpx
10
+ alpha_vantage
11
+ fastapi
12
+ uvicorn[standard]
13
+ tavily
14
+ langchain_ollama
15
+ langchain-google-genai
start_all.sh ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ # Function to kill all background processes on exit
4
+ cleanup() {
5
+ echo "Stopping all services..."
6
+ kill $(jobs -p) 2>/dev/null
7
+ exit
8
+ }
9
+
10
+ # Trap SIGINT (Ctrl+C) and call cleanup
11
+ trap cleanup SIGINT
12
+
13
+ # Cleanup existing processes to prevent port conflicts
14
+ echo "🧹 Cleaning up existing processes..."
15
+ lsof -ti:8000,8001,8002,8003,8501 | xargs kill -9 2>/dev/null || true
16
+ pkill -f "uvicorn" || true
17
+ pkill -f "streamlit" || true
18
+ sleep 2
19
+
20
+ echo "🚀 Starting Aegis System..."
21
+
22
+ # Check if venv exists and activate it
23
+ if [ -d "venv" ]; then
24
+ echo "🔌 Activating virtual environment..."
25
+ source venv/bin/activate
26
+ else
27
+ echo "⚠️ No virtual environment found. Running with system python..."
28
+ fi
29
+
30
+ # Start Microservices
31
+ echo "Starting MCP Gateway (Port 8000)..."
32
+ python mcp_gateway.py > mcp_gateway.log 2>&1 &
33
+
34
+ echo "Starting Tavily MCP (Port 8001)..."
35
+ python tavily_mcp.py > tavily_mcp.log 2>&1 &
36
+
37
+ echo "Starting Alpha Vantage MCP (Port 8002)..."
38
+ python alphavantage_mcp.py > alphavantage_mcp.log 2>&1 &
39
+
40
+ echo "Starting Private Portfolio MCP (Port 8003)..."
41
+ python private_mcp.py > private_mcp.log 2>&1 &
42
+
43
+ # Start Monitor
44
+ echo "Starting Proactive Monitor..."
45
+ python monitor.py > monitor.log 2>&1 &
46
+
47
+ # Wait a moment for services to spin up
48
+ sleep 3
49
+
50
+ # Start Streamlit App
51
+ echo "🛡️ Launching Sentinel Interface..."
52
+ streamlit run app.py > streamlit.log 2>&1
style.css ADDED
@@ -0,0 +1,212 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap');
2
+
3
+ :root {
4
+ /* New Theme based on latest image - "Deep Dark" */
5
+ --bg-color: #000000;
6
+ --card-bg: #0c0c0c;
7
+ /* Slightly lighter than black */
8
+ --sidebar-bg: #000000;
9
+ --border-color: #1e1e1e;
10
+ --text-primary: #ffffff;
11
+ --text-secondary: #9ca3af;
12
+ /* Cool grey */
13
+
14
+ /* Accents - Minimalist */
15
+ --primary: #ffffff;
16
+ --primary-hover: #e5e5e5;
17
+
18
+ /* Semantic */
19
+ --success: #10b981;
20
+ --warning: #f59e0b;
21
+ --danger: #ef4444;
22
+
23
+ --font-main: 'Inter', sans-serif;
24
+ /* Switched to Inter for cleaner look */
25
+ --font-mono: 'JetBrains Mono', monospace;
26
+ }
27
+
28
+ /* Global Reset */
29
+ .stApp {
30
+ background-color: var(--bg-color);
31
+ color: var(--text-primary);
32
+ font-family: var(--font-main);
33
+ }
34
+
35
+ h1,
36
+ h2,
37
+ h3,
38
+ h4,
39
+ h5,
40
+ h6 {
41
+ font-family: var(--font-main);
42
+ font-weight: 600;
43
+ color: var(--text-primary);
44
+ letter-spacing: -0.02em;
45
+ }
46
+
47
+ /* Sidebar */
48
+ section[data-testid="stSidebar"] {
49
+ background-color: var(--sidebar-bg);
50
+ border-right: 1px solid var(--border-color);
51
+ }
52
+
53
+ /* Feature Cards - Exact Match Attempt */
54
+ .feature-grid {
55
+ display: grid;
56
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
57
+ gap: 24px;
58
+ /* Specific gap */
59
+ margin-top: 3rem;
60
+ padding: 0 1rem;
61
+ }
62
+
63
+ .feature-card {
64
+ background-color: #121212;
65
+ /* Material Dark Surface - Visible contrast against black */
66
+ border: 1px solid #333333;
67
+ /* More visible border */
68
+ border-radius: 16px;
69
+ /* Slightly more rounded */
70
+ padding: 40px;
71
+ /* Generous padding as seen in image */
72
+ display: flex;
73
+ flex-direction: column;
74
+ align-items: flex-start;
75
+ height: 100%;
76
+ transition: all 0.2s ease;
77
+ }
78
+
79
+ .feature-card:hover {
80
+ border-color: #333;
81
+ background-color: #0a0a0a;
82
+ transform: translateY(-2px);
83
+ }
84
+
85
+ .feature-icon {
86
+ font-size: 3rem;
87
+ /* Larger icons */
88
+ margin-bottom: 24px;
89
+ line-height: 1;
90
+ }
91
+
92
+ .feature-title {
93
+ font-size: 1.35rem;
94
+ /* Slightly larger title */
95
+ font-weight: 700;
96
+ margin-bottom: 16px;
97
+ color: #ffffff;
98
+ letter-spacing: -0.01em;
99
+ }
100
+
101
+ .feature-desc {
102
+ font-size: 1rem;
103
+ color: #888888;
104
+ /* Muted grey description */
105
+ line-height: 1.6;
106
+ font-weight: 400;
107
+ }
108
+
109
+ /* Buttons */
110
+ .stButton button {
111
+ background-color: white;
112
+ color: black;
113
+ border: none;
114
+ font-weight: 500;
115
+ border-radius: 6px;
116
+ /* Slightly squarer */
117
+ padding: 0.75rem 2rem;
118
+ transition: all 0.2s ease;
119
+ }
120
+
121
+ .stButton button:hover {
122
+ background-color: #e5e5e5;
123
+ }
124
+
125
+ /* Inputs */
126
+ .stTextInput input,
127
+ .stTextArea textarea,
128
+ .stSelectbox div[data-baseweb="select"] {
129
+ background-color: #111 !important;
130
+ color: white !important;
131
+ border: 1px solid #333 !important;
132
+ border-radius: 6px;
133
+ }
134
+
135
+ .stTextInput input:focus,
136
+ .stTextArea textarea:focus {
137
+ border-color: #666 !important;
138
+ }
139
+
140
+ /* Hero */
141
+ .hero-container {
142
+ display: flex;
143
+ flex-direction: column;
144
+ align-items: center;
145
+ justify-content: center;
146
+ text-align: center;
147
+ padding: 6rem 1rem 4rem 1rem;
148
+ width: 100%;
149
+ margin: 0 auto;
150
+ }
151
+
152
+ .hero-title {
153
+ font-size: 4.5rem;
154
+ font-weight: 800;
155
+ margin-bottom: 1.5rem;
156
+ color: white;
157
+ background: none;
158
+ /* Removed gradient for solid white punch */
159
+ -webkit-text-fill-color: white;
160
+ text-align: center;
161
+ width: 100%;
162
+ letter-spacing: -0.03em;
163
+ }
164
+
165
+ .hero-subtitle {
166
+ font-size: 1.25rem;
167
+ color: var(--text-secondary);
168
+ max-width: 700px;
169
+ margin: 0 auto 2.5rem auto;
170
+ text-align: center;
171
+ line-height: 1.6;
172
+ }
173
+
174
+ /* Alert Cards */
175
+ .alert-card {
176
+ background: var(--card-bg);
177
+ border: 1px solid var(--border-color);
178
+ padding: 16px;
179
+ margin-bottom: 12px;
180
+ border-radius: 8px;
181
+ transition: border-color 0.2s ease;
182
+ }
183
+
184
+ .alert-card:hover {
185
+ border-color: #333;
186
+ }
187
+
188
+ /* Market alerts - Red accent */
189
+ .alert-market {
190
+ border-left: 3px solid #ef4444;
191
+ background: linear-gradient(90deg, rgba(239, 68, 68, 0.05) 0%, var(--card-bg) 100%);
192
+ }
193
+
194
+ /* News alerts - Blue accent */
195
+ .alert-news {
196
+ border-left: 3px solid #3b82f6;
197
+ background: linear-gradient(90deg, rgba(59, 130, 246, 0.05) 0%, var(--card-bg) 100%);
198
+ }
199
+
200
+ .alert-header {
201
+ display: flex;
202
+ justify-content: space-between;
203
+ color: var(--text-secondary);
204
+ font-size: 0.85rem;
205
+ margin-bottom: 8px;
206
+ font-weight: 500;
207
+ }
208
+
209
+ .alert-body {
210
+ color: var(--text-primary);
211
+ font-size: 0.95rem;
212
+ }
tavily_mcp.py ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # tavily_mcp.py (Corrected Version)
2
+ from fastapi import FastAPI, HTTPException
3
+ import uvicorn
4
+ import os
5
+ from dotenv import load_dotenv
6
+ from tavily import TavilyClient
7
+ import logging
8
+
9
+ # --- Configuration ---
10
+ load_dotenv()
11
+
12
+ # --- Logging Setup (MUST be before we use logger) ---
13
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
14
+ logger = logging.getLogger("Tavily_MCP_Server")
15
+
16
+ # --- Get API Key ---
17
+ TAVILY_API_KEY = os.getenv("TAVILY_API_KEY")
18
+
19
+ # Fallback: Try to read from Streamlit secrets file (for cloud deployment)
20
+ if not TAVILY_API_KEY:
21
+ try:
22
+ import toml
23
+ secrets_path = os.path.join(os.path.dirname(__file__), ".streamlit", "secrets.toml")
24
+ if os.path.exists(secrets_path):
25
+ secrets = toml.load(secrets_path)
26
+ TAVILY_API_KEY = secrets.get("TAVILY_API_KEY")
27
+ logger.info("Loaded TAVILY_API_KEY from .streamlit/secrets.toml")
28
+ except Exception as e:
29
+ logger.warning(f"Could not load from secrets.toml: {e}")
30
+
31
+ if not TAVILY_API_KEY:
32
+ logger.warning("TAVILY_API_KEY not found in environment. Search features will fail.")
33
+ else:
34
+ logger.info(f"TAVILY_API_KEY found: {TAVILY_API_KEY[:4]}...")
35
+
36
+ # --- FastAPI App & Tavily Client ---
37
+ app = FastAPI(title="Aegis Tavily MCP Server")
38
+ tavily = TavilyClient(api_key=TAVILY_API_KEY)
39
+
40
+ @app.post("/research")
41
+ async def perform_research(payload: dict):
42
+ """
43
+ Performs a search for each query using the Tavily API.
44
+ Expects a payload like:
45
+ {
46
+ "queries": ["query1", "query2"],
47
+ "search_depth": "basic" or "advanced" (optional, default basic)
48
+ }
49
+ """
50
+ queries = payload.get("queries")
51
+ search_depth = payload.get("search_depth", "basic")
52
+
53
+ if not queries or not isinstance(queries, list):
54
+ logger.error("Validation Error: 'queries' must be a non-empty list.")
55
+ raise HTTPException(status_code=400, detail="'queries' must be a non-empty list.")
56
+
57
+ logger.info(f"Received research request for {len(queries)} queries. Search depth: {search_depth}")
58
+
59
+ # --- THIS IS THE CORRECTED LOGIC ---
60
+ all_results = []
61
+ try:
62
+ # Loop through each query and perform a search
63
+ for query in queries:
64
+ logger.info(f"Performing search for query: '{query}'")
65
+ # The search method takes a single query string
66
+ response = tavily.search(
67
+ query=query,
68
+ search_depth=search_depth,
69
+ max_results=5
70
+ )
71
+ # Add the results for this query to our collection
72
+ all_results.append({"query": query, "results": response["results"]})
73
+
74
+ logger.info(f"Successfully retrieved results for all queries from Tavily API.")
75
+ return {"status": "success", "data": all_results}
76
+
77
+ except Exception as e:
78
+ logger.error(f"Tavily API Error (likely rate limit): {e}. Switching to MOCK DATA fallback.")
79
+ # --- FALLBACK MECHANISM ---
80
+ mock_results = []
81
+ import random
82
+ from datetime import datetime
83
+
84
+ # Dynamic market sentiments to rotate through
85
+ sentiments = ["Bullish", "Bearish", "Neutral", "Volatile", "Cautious"]
86
+ events = ["Earnings Surprise", "New Product Launch", "Regulatory Update", "Sector Rotation", "Macro Headwinds"]
87
+
88
+ current_time = datetime.now().strftime("%H:%M")
89
+
90
+ for query in queries:
91
+ # Pick random sentiment and event to diversify the "news"
92
+ s = random.choice(sentiments)
93
+ e = random.choice(events)
94
+
95
+ mock_results.append({
96
+ "query": query,
97
+ "results": [
98
+ {
99
+ "title": f"[{current_time}] Market Update: {s} Sentiment for {query}",
100
+ "content": f"Live market data at {current_time} indicates a {s} trend for {query}. Analysts are tracking a potential {e} that could impact short-term price action. Volume remains high as traders adjust positions.",
101
+ "url": "http://mock-source.com/market-update"
102
+ },
103
+ {
104
+ "title": f"[{current_time}] Sector Alert: {e} affecting {query}",
105
+ "content": f"Breaking: A significant {e} is rippling through the sector, heavily influencing {query}. Experts advise monitoring key resistance levels. (Simulated Real-Time Data)",
106
+ "url": "http://mock-source.com/sector-alert"
107
+ }
108
+ ]
109
+ })
110
+ return {"status": "success", "data": mock_results}
111
+
112
+ @app.get("/")
113
+ def read_root():
114
+ return {"message": "Aegis Tavily MCP Server is operational."}
115
+
116
+ if __name__ == "__main__":
117
+ uvicorn.run(app, host="127.0.0.1", port=8001)
test_ollama.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import requests
2
+
3
+ def check_ollama():
4
+ try:
5
+ response = requests.get("http://localhost:11434/")
6
+ if response.status_code == 200:
7
+ print("✅ Ollama is running.")
8
+ else:
9
+ print(f"⚠️ Ollama returned status code: {response.status_code}")
10
+
11
+ # Check for models
12
+ response = requests.get("http://localhost:11434/api/tags")
13
+ if response.status_code == 200:
14
+ models = [m['name'] for m in response.json().get('models', [])]
15
+ print(f"Available models: {models}")
16
+ if "llama3:latest" in models or "llama3" in models:
17
+ print("✅ llama3 model found.")
18
+ else:
19
+ print("❌ llama3 model NOT found. Please run 'ollama pull llama3'")
20
+ else:
21
+ print("❌ Could not list models.")
22
+
23
+ except Exception as e:
24
+ print(f"❌ Error connecting to Ollama: {e}")
25
+
26
+ if __name__ == "__main__":
27
+ check_ollama()
watchlist.json ADDED
@@ -0,0 +1 @@
 
 
1
+ ["AAPL", "TSLA", "NVDA", "MSFT", "AMZN", "GOOGL"]