CiscsoPonce commited on
Commit
82de0b8
·
1 Parent(s): 5c8029a

Feat: Add Brave, Charts and Email Notifications

Browse files
Files changed (6) hide show
  1. app.py +38 -33
  2. requirements.txt +3 -1
  3. src/agent.py +41 -50
  4. src/email_utils.py +39 -0
  5. src/viz.py +34 -0
  6. test_llm.py +1 -1
app.py CHANGED
@@ -1,57 +1,62 @@
1
  import chainlit as cl
2
- from src.agent import app # Import the robot you built
3
 
4
  @cl.on_chat_start
5
  async def start():
6
- """
7
- Sends a welcome message when the user opens the website.
8
- """
9
- await cl.Message(content="👋 **Hello! I am PrimoGreedy.**\n\nI am your skeptical financial analyst.\nGive me a ticker (e.g., AAPL, AMC, PLTR) and I will run it through the Firewall.").send()
10
 
11
  @cl.on_message
12
  async def main(message: cl.Message):
13
- """
14
- Runs every time the user types a message.
15
- """
16
- ticker = message.content.upper().strip()
17
 
18
- # 1. Notify user we are starting
19
- msg = cl.Message(content=f"🔍 Analyzing **{ticker}**...")
 
 
 
20
  await msg.send()
21
 
22
- # 2. Run the Agent Logic
23
- # We use 'invoke' to run the graph we built in agent.py
24
  try:
25
- result = await app.ainvoke({"ticker": ticker})
 
26
 
27
- # 3. Check the Result
28
- final_report = result.get('final_report')
29
  status = result.get('status')
30
- financial_data = result.get('financial_data', {})
31
-
32
- # 4. Display the Output
 
 
 
 
 
 
 
33
  if status == "FAIL":
34
- # If the Firewall rejected it
35
- reason = financial_data.get('reason', 'Unknown Reason')
36
-
37
  response = f"""
38
- ❌ **REJECTED BY FIREWALL**
39
-
40
- **Ticker:** {ticker}
41
- **Reason:** {reason}
42
 
43
- *I did not waste time searching for news because this stock is too risky.*
44
  """
45
- else:
46
- # If it passed and the LLM wrote a report
47
  response = f"""
48
  ✅ **PASSED FIREWALL**
49
 
50
- {final_report}
51
- """
52
 
53
- # Send the final answer to the chat UI
54
- await cl.Message(content=response).send()
 
 
 
 
 
 
 
 
55
 
56
  except Exception as e:
57
  await cl.Message(content=f"⚠️ Error: {str(e)}").send()
 
1
  import chainlit as cl
2
+ from src.agent import app
3
 
4
  @cl.on_chat_start
5
  async def start():
6
+ # Send a Welcome Message that lists your new powers
7
+ await cl.Message(content="👋 **PrimoGreedy v3.0**\n\n- **Brave Search** Active 🦁\n- **Resend Email** Active 📧\n- **Charts** Active 📈\n\n*Type a ticker (e.g., NVDA) to scout, or just say Hello.*").send()
 
 
8
 
9
  @cl.on_message
10
  async def main(message: cl.Message):
11
+ user_input = message.content.strip()
 
 
 
12
 
13
+ # Simple check: If it looks like a ticker, say "Scouting", otherwise "Thinking"
14
+ if len(user_input) <= 5 and " " not in user_input:
15
+ msg = cl.Message(content=f"🔍 Scouting **{user_input.upper()}**...")
16
+ else:
17
+ msg = cl.Message(content=f"🤔 Thinking...")
18
  await msg.send()
19
 
 
 
20
  try:
21
+ # Run the full Agent (Brain + Eyes + Hands)
22
+ result = await app.ainvoke({"ticker": user_input})
23
 
24
+ # Extract all the new data we added
 
25
  status = result.get('status')
26
+ report = result.get('final_report')
27
+ chart_bytes = result.get('chart_data')
28
+ email_msg = result.get('email_status')
29
+
30
+ # Prepare the Image (if we have one)
31
+ elements = []
32
+ if chart_bytes:
33
+ elements.append(cl.Image(content=chart_bytes, name="chart", display="inline"))
34
+
35
+ # Format the Text Response
36
  if status == "FAIL":
37
+ # Rejection (Firewall)
 
 
38
  response = f"""
39
+ ❌ **REJECTED**: {result.get('financial_data', {}).get('reason')}
 
 
 
40
 
41
+ *No email sent. No chart drawn.*
42
  """
43
+ elif status == "PASS":
44
+ # Success (Analysis + Chart + Email)
45
  response = f"""
46
  ✅ **PASSED FIREWALL**
47
 
48
+ {report}
 
49
 
50
+ ---
51
+ **System Status:**
52
+ {email_msg if email_msg else "📧 Email not sent (Check keys)"}
53
+ """
54
+ else:
55
+ # Just Chatting
56
+ response = report
57
+
58
+ # Send everything to the UI
59
+ await cl.Message(content=response, elements=elements).send()
60
 
61
  except Exception as e:
62
  await cl.Message(content=f"⚠️ Error: {str(e)}").send()
requirements.txt CHANGED
@@ -7,4 +7,6 @@ langgraph
7
  yfinance
8
  duckduckgo-search
9
  openai
10
- requests
 
 
 
7
  yfinance
8
  duckduckgo-search
9
  openai
10
+ requests
11
+ matplotlib
12
+ resend
src/agent.py CHANGED
@@ -1,105 +1,96 @@
1
  import os
2
- from typing import TypedDict, Optional
3
  from langgraph.graph import StateGraph, END
4
  from langchain_core.messages import HumanMessage, SystemMessage
5
  import requests
6
  from src.llm import get_llm
7
  from src.finance_tools import check_financial_health
 
 
8
 
9
- # --- 1. STATE DEFINITION ---
10
  class AgentState(TypedDict):
11
  ticker: str
12
  status: str
13
  financial_data: Optional[dict]
14
  final_report: Optional[str]
 
 
15
 
16
  llm = get_llm()
17
 
18
- # --- 2. BRAVE SEARCH TOOL ---
19
  def brave_market_search(query: str):
20
- """Fetches real-time market sentiment using Brave Search API."""
21
  api_key = os.getenv("BRAVE_API_KEY")
22
- if not api_key:
23
- return "Brave API Key missing. Skipping web search."
24
 
25
  url = "https://api.search.brave.com/res/v1/web/search"
26
- headers = {
27
- "Accept": "application/json",
28
- "X-Subscription-Token": api_key
29
- }
30
- params = {"q": f"{query} stock market news sentiment", "count": 5}
31
 
32
  try:
33
- response = requests.get(url, headers=headers, params=params)
34
- results = response.json().get("web", {}).get("results", [])
35
- snippets = [f"- {r['title']}: {r['description']}" for r in results]
36
- return "\n".join(snippets) if snippets else "No recent news found."
37
  except Exception as e:
38
- return f"Search error: {str(e)}"
39
 
40
  # --- 3. NODES ---
41
-
42
  def check_health(state: AgentState):
43
- """The Logic Firewall Node."""
44
  ticker = state['ticker'].upper()
45
  result = check_financial_health(ticker)
46
  return {"financial_data": result, "status": result['status']}
47
 
48
  async def analyze_stock(state: AgentState):
49
- """The AI Research Node (Now with Brave Search)."""
50
- if state['status'] == "FAIL":
51
- return state
52
 
53
  ticker = state['ticker'].upper()
54
- # Use Brave for real-time data
55
- market_news = brave_market_search(ticker)
56
 
57
- prompt = f"""
58
- You are a professional stock analyst.
59
- Analyze {ticker} based on these financials and real-time news.
60
 
61
- Financial Health: {state['financial_data']['reason']}
62
- Recent Market Sentiment (via Brave):
63
- {market_news}
 
 
64
 
65
- Provide a concise 'Recommendation' (BUY/HOLD/SELL) and a brief justification.
66
  """
 
 
 
 
 
 
 
67
 
68
- response = await llm.ainvoke([SystemMessage(content="You are a skeptical analyst."), HumanMessage(content=prompt)])
69
- return {"final_report": response.content}
 
 
 
70
 
71
  async def chat_mode(state: AgentState):
72
- """The Conversational Node."""
73
- prompt = [
74
- SystemMessage(content="You are PrimoGreedy, a witty and skeptical financial assistant. Keep it brief."),
75
- HumanMessage(content=state['ticker'])
76
- ]
77
- response = await llm.ainvoke(prompt)
78
  return {"final_report": response.content, "status": "CHAT"}
79
 
80
- # --- 4. THE ROUTER ---
81
  def route_query(state: AgentState):
82
  query = state['ticker'].strip().upper()
83
- # Simple logic: If 1-5 chars and no spaces, it's likely a ticker
84
  if 1 <= len(query) <= 5 and " " not in query:
85
  return "financial_health_check"
86
  return "chat_mode"
87
 
88
- # --- 5. BUILD THE GRAPH ---
89
  workflow = StateGraph(AgentState)
90
-
91
  workflow.add_node("financial_health_check", check_health)
92
  workflow.add_node("analyst_research", analyze_stock)
93
  workflow.add_node("chat_mode", chat_mode)
94
 
95
- workflow.set_conditional_entry_point(
96
- route_query,
97
- {
98
- "financial_health_check": "financial_health_check",
99
- "chat_mode": "chat_mode"
100
- }
101
- )
102
-
103
  workflow.add_edge("financial_health_check", "analyst_research")
104
  workflow.add_edge("analyst_research", END)
105
  workflow.add_edge("chat_mode", END)
 
1
  import os
2
+ from typing import TypedDict, Optional, Any
3
  from langgraph.graph import StateGraph, END
4
  from langchain_core.messages import HumanMessage, SystemMessage
5
  import requests
6
  from src.llm import get_llm
7
  from src.finance_tools import check_financial_health
8
+ from src.viz import get_stock_chart
9
+ from src.email_utils import send_email_report
10
 
11
+ # --- 1. STATE ---
12
  class AgentState(TypedDict):
13
  ticker: str
14
  status: str
15
  financial_data: Optional[dict]
16
  final_report: Optional[str]
17
+ chart_data: Optional[Any]
18
+ email_status: Optional[str]
19
 
20
  llm = get_llm()
21
 
22
+ # --- 2. TOOLS ---
23
  def brave_market_search(query: str):
24
+ """Uses Brave API to find real-time news."""
25
  api_key = os.getenv("BRAVE_API_KEY")
26
+ if not api_key: return "⚠️ Brave Key Missing."
 
27
 
28
  url = "https://api.search.brave.com/res/v1/web/search"
29
+ headers = {"X-Subscription-Token": api_key}
30
+ params = {"q": f"{query} stock news sentiment", "count": 3}
 
 
 
31
 
32
  try:
33
+ data = requests.get(url, headers=headers, params=params).json()
34
+ results = data.get("web", {}).get("results", [])
35
+ return "\n".join([f"- {r['title']}" for r in results])
 
36
  except Exception as e:
37
+ return f"Search Error: {str(e)}"
38
 
39
  # --- 3. NODES ---
 
40
  def check_health(state: AgentState):
 
41
  ticker = state['ticker'].upper()
42
  result = check_financial_health(ticker)
43
  return {"financial_data": result, "status": result['status']}
44
 
45
  async def analyze_stock(state: AgentState):
46
+ if state['status'] == "FAIL": return state
 
 
47
 
48
  ticker = state['ticker'].upper()
 
 
49
 
50
+ # 1. Run Tools
51
+ news = brave_market_search(ticker)
52
+ chart = get_stock_chart(ticker)
53
 
54
+ # 2. AI Analysis
55
+ prompt = f"""
56
+ Analyze {ticker}.
57
+ Financials: {state['financial_data']['reason']}
58
+ Real-Time News (Brave): {news}
59
 
60
+ Give a recommendation (BUY/HOLD/SELL) and detailed reasoning.
61
  """
62
+ response = await llm.ainvoke([
63
+ SystemMessage(content="You are PrimoGreedy. Sarcastic, skeptical, data-driven."),
64
+ HumanMessage(content=prompt)
65
+ ])
66
+
67
+ # 3. Send Email (Only if it passed the firewall)
68
+ email_result = send_email_report(ticker, response.content)
69
 
70
+ return {
71
+ "final_report": response.content,
72
+ "chart_data": chart,
73
+ "email_status": email_result
74
+ }
75
 
76
  async def chat_mode(state: AgentState):
77
+ response = await llm.ainvoke([HumanMessage(content=state['ticker'])])
 
 
 
 
 
78
  return {"final_report": response.content, "status": "CHAT"}
79
 
80
+ # --- 4. ROUTER ---
81
  def route_query(state: AgentState):
82
  query = state['ticker'].strip().upper()
 
83
  if 1 <= len(query) <= 5 and " " not in query:
84
  return "financial_health_check"
85
  return "chat_mode"
86
 
87
+ # --- 5. GRAPH ---
88
  workflow = StateGraph(AgentState)
 
89
  workflow.add_node("financial_health_check", check_health)
90
  workflow.add_node("analyst_research", analyze_stock)
91
  workflow.add_node("chat_mode", chat_mode)
92
 
93
+ workflow.set_conditional_entry_point(route_query, {"financial_health_check": "financial_health_check", "chat_mode": "chat_mode"})
 
 
 
 
 
 
 
94
  workflow.add_edge("financial_health_check", "analyst_research")
95
  workflow.add_edge("analyst_research", END)
96
  workflow.add_edge("chat_mode", END)
src/email_utils.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import resend
3
+
4
+ def send_email_report(ticker: str, report: str):
5
+ """
6
+ Sends an email alert using Resend.
7
+ """
8
+ api_key = os.getenv("RESEND_API_KEY")
9
+ target_email = os.getenv("TARGET_EMAIL") # We will set this in Hugging Face Secrets
10
+
11
+ if not api_key or not target_email:
12
+ return "⚠️ Email skipped: Missing API Key or Target Email."
13
+
14
+ resend.api_key = api_key
15
+
16
+ # Format the email body (Convert newlines to HTML breaks)
17
+ html_body = f"""
18
+ <h1>PrimoGreedy Report: {ticker}</h1>
19
+ <p><strong>Status:</strong> PASSED FIREWALL</p>
20
+ <hr>
21
+ <div style="font-family: monospace; white-space: pre-wrap;">
22
+ {report}
23
+ </div>
24
+ <hr>
25
+ <p><em>Generated by your AI Agent.</em></p>
26
+ """
27
+
28
+ params = {
29
+ "from": "PrimoGreedy <onboarding@resend.dev>", # Default testing domain
30
+ "to": [target_email],
31
+ "subject": f"💰 Trade Alert: {ticker} Analysis",
32
+ "html": html_body
33
+ }
34
+
35
+ try:
36
+ email = resend.Emails.send(params)
37
+ return f"📧 Email sent! (ID: {email.get('id')})"
38
+ except Exception as e:
39
+ return f"⚠️ Email failed: {str(e)}"
src/viz.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import matplotlib.pyplot as plt
2
+ import yfinance as yf
3
+ import io
4
+
5
+ def get_stock_chart(ticker: str):
6
+ """
7
+ Generates a 6-month price chart and returns the image as bytes.
8
+ """
9
+ try:
10
+ stock = yf.Ticker(ticker)
11
+ hist = stock.history(period="6mo")
12
+
13
+ if hist.empty: return None
14
+
15
+ plt.figure(figsize=(10, 5))
16
+ plt.plot(hist.index, hist['Close'], label='Price', color='#00ff00', linewidth=2)
17
+ plt.title(f"{ticker} - 6 Month Trend", color='white')
18
+ plt.grid(True, alpha=0.3)
19
+
20
+ # Dark Mode Style
21
+ ax = plt.gca()
22
+ ax.set_facecolor('#0e1117')
23
+ plt.gcf().set_facecolor('#0e1117')
24
+ ax.tick_params(colors='white')
25
+
26
+ # Save to RAM
27
+ buf = io.BytesIO()
28
+ plt.savefig(buf, format='png', bbox_inches='tight')
29
+ plt.close()
30
+ buf.seek(0)
31
+ return buf.getvalue()
32
+
33
+ except:
34
+ return None
test_llm.py CHANGED
@@ -4,7 +4,7 @@ print("\n--- 🧠 PRIMOGREEDY BRAIN TEST ---\n")
4
 
5
  try:
6
  llm = get_llm()
7
- print("🤖 Asking Llama 3: 'What is the most dangerous risk for a bank?'...")
8
 
9
  response = llm.invoke("What is the single most dangerous risk for a bank? Answer in 1 sentence.")
10
 
 
4
 
5
  try:
6
  llm = get_llm()
7
+ print("🤖 Asking Agent: 'What is the most dangerous risk for a bank?'...")
8
 
9
  response = llm.invoke("What is the single most dangerous risk for a bank? Answer in 1 sentence.")
10