tosanoob commited on
Commit
44f095f
·
1 Parent(s): e48425a

feat: add news report

Browse files
.gitignore CHANGED
@@ -1,3 +1,4 @@
1
  .env
2
  PLAN.md
 
3
  __pycache__/
 
1
  .env
2
  PLAN.md
3
+ PLAN_UPDATE.md
4
  __pycache__/
Home.py CHANGED
@@ -46,20 +46,20 @@ if hasattr(st, 'empty'):
46
 
47
  ### Main Features:
48
 
49
- 1. **💬 Chat with AI Financial Analyst**:
50
- - Search for stock information
51
- - View price charts
52
- - Convert currencies
53
   
54
- 2. **📄 In-depth Stock Analysis Report**:
55
  - Comprehensive analysis of a specific stock
56
  - Data collection from multiple sources
57
  - Generate in-depth reports with AI evaluation
58
   
59
- 3. **📰 Daily Market Summary Newsletter** (Coming soon):
60
- - Compilation of latest financial news
61
- - Categorized by topic
62
- - Daily market updates
63
 
64
  ### How to Use:
65
 
 
46
 
47
  ### Main Features:
48
 
49
+ 1. **📰 Daily News Report** - Summary of the latest financial news:
50
+ - Compilation of latest financial news
51
+ - Categorized by topic
52
+ - Daily market updates
53
   
54
+ 2. **📄 Stock Analysis Report** - In-depth analysis of a specific stock:
55
  - Comprehensive analysis of a specific stock
56
  - Data collection from multiple sources
57
  - Generate in-depth reports with AI evaluation
58
   
59
+ 3. **💬 Stock Chatbot** - Chat with AI Financial Analyst:
60
+ - Search for stock information
61
+ - View price charts
62
+ - Convert currencies
63
 
64
  ### How to Use:
65
 
env-example CHANGED
@@ -9,4 +9,8 @@ NGROK_STATIC_DOMAIN=
9
  # Invest insight app
10
  ALPHA_VANTAGE_API_KEY=
11
  NEWS_API_KEY=
12
- MARKETAUX_API_KEY=
 
 
 
 
 
9
  # Invest insight app
10
  ALPHA_VANTAGE_API_KEY=
11
  NEWS_API_KEY=
12
+ MARKETAUX_API_KEY=
13
+
14
+ # Email
15
+ SENDER_APP_PASSWORD=
16
+ SENDER_EMAIL=<email_address>@gmail.com
modules/email_sender.py ADDED
@@ -0,0 +1,226 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import smtplib
3
+ import tempfile
4
+ from email.mime.multipart import MIMEMultipart
5
+ from email.mime.text import MIMEText
6
+ from email.mime.application import MIMEApplication
7
+ from dotenv import load_dotenv
8
+ from markdown_it import MarkdownIt
9
+ from datetime import datetime
10
+ import weasyprint
11
+
12
+ # Tải biến môi trường
13
+ load_dotenv()
14
+
15
+ # Thông tin email từ biến môi trường
16
+ SENDER_EMAIL = os.getenv("SENDER_EMAIL")
17
+ SENDER_APP_PASSWORD = os.getenv("SENDER_APP_PASSWORD")
18
+
19
+ def _markdown_to_html(markdown_string):
20
+ """Convert Markdown to HTML"""
21
+ md = MarkdownIt()
22
+ html_content = md.render(markdown_string)
23
+
24
+ # Format current date
25
+ current_date = datetime.now().strftime("%d/%m/%Y")
26
+
27
+ # Create complete HTML with CSS for nice formatting
28
+ full_html = f"""
29
+ <!DOCTYPE html>
30
+ <html>
31
+ <head>
32
+ <meta charset="UTF-8">
33
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
34
+ <title>Daily Market Report - {current_date}</title>
35
+ <style>
36
+ @page {{
37
+ size: A4;
38
+ margin: 2cm;
39
+ }}
40
+ body {{
41
+ font-family: Arial, Helvetica, sans-serif;
42
+ line-height: 1.5;
43
+ color: #333;
44
+ max-width: 800px;
45
+ margin: 0 auto;
46
+ }}
47
+ .report-header {{
48
+ text-align: center;
49
+ margin-bottom: 30px;
50
+ }}
51
+ .report-date {{
52
+ font-style: italic;
53
+ color: #666;
54
+ margin-bottom: 10px;
55
+ }}
56
+ .report-title {{
57
+ font-size: 24pt;
58
+ margin-bottom: 5px;
59
+ color: #2c3e50;
60
+ }}
61
+ .report-subtitle {{
62
+ font-size: 14pt;
63
+ color: #7f8c8d;
64
+ margin-top: 0;
65
+ }}
66
+ .report-body {{
67
+ text-align: justify;
68
+ }}
69
+ h1, h2, h3, h4, h5, h6 {{
70
+ color: #2c3e50;
71
+ margin-top: 20px;
72
+ }}
73
+ h1 {{ font-size: 20pt; }}
74
+ h2 {{ font-size: 18pt; }}
75
+ h3 {{ font-size: 16pt; }}
76
+ h4 {{ font-size: 14pt; }}
77
+ h5 {{ font-size: 12pt; }}
78
+ h6 {{ font-size: 10pt; }}
79
+
80
+ p {{
81
+ margin-bottom: 10px;
82
+ }}
83
+
84
+ a {{
85
+ color: #3498db;
86
+ text-decoration: none;
87
+ }}
88
+
89
+ a:hover {{
90
+ text-decoration: underline;
91
+ }}
92
+
93
+ ul, ol {{
94
+ margin: 10px 0 10px 20px;
95
+ }}
96
+
97
+ li {{
98
+ margin-bottom: 5px;
99
+ }}
100
+
101
+ blockquote {{
102
+ border-left: 4px solid #eee;
103
+ padding-left: 10px;
104
+ margin-left: 0;
105
+ color: #777;
106
+ }}
107
+
108
+ .section {{
109
+ margin-bottom: 30px;
110
+ }}
111
+
112
+ .footer {{
113
+ text-align: center;
114
+ margin-top: 40px;
115
+ padding-top: 20px;
116
+ font-size: 12px;
117
+ color: #777;
118
+ border-top: 1px solid #eee;
119
+ }}
120
+
121
+ /* Custom styling for bullet points */
122
+ ul {{
123
+ list-style-type: disc;
124
+ }}
125
+ ul ul {{
126
+ list-style-type: circle;
127
+ }}
128
+ ul ul ul {{
129
+ list-style-type: square;
130
+ }}
131
+ </style>
132
+ </head>
133
+ <body>
134
+ <div class="report-header">
135
+ <div class="report-date">Date: {current_date}</div>
136
+ <h1 class="report-title">Daily Market Report</h1>
137
+ <h2 class="report-subtitle">AI Financial Dashboard</h2>
138
+ </div>
139
+
140
+ <div class="report-body">
141
+ {html_content}
142
+ </div>
143
+
144
+ <div class="footer">
145
+ This report was automatically generated by AI Financial Dashboard. Information is for reference only.
146
+ </div>
147
+ </body>
148
+ </html>
149
+ """
150
+
151
+ return full_html
152
+
153
+ def _generate_pdf_from_markdown(markdown_string):
154
+ """Generate PDF from Markdown using WeasyPrint"""
155
+ # Convert markdown to HTML
156
+ html_content = _markdown_to_html(markdown_string)
157
+
158
+ # Create temporary HTML file
159
+ with tempfile.NamedTemporaryFile(suffix='.html', delete=False) as temp_html:
160
+ temp_html_path = temp_html.name
161
+ temp_html.write(html_content.encode('utf-8'))
162
+
163
+ # Create PDF from HTML
164
+ try:
165
+ # Create temporary PDF filename
166
+ with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as temp_pdf:
167
+ temp_pdf_path = temp_pdf.name
168
+
169
+ # Generate PDF
170
+ weasyprint.HTML(filename=temp_html_path).write_pdf(temp_pdf_path)
171
+
172
+ # Read PDF content
173
+ with open(temp_pdf_path, 'rb') as f:
174
+ pdf_data = f.read()
175
+
176
+ # Delete temporary files
177
+ os.unlink(temp_html_path)
178
+ os.unlink(temp_pdf_path)
179
+
180
+ return pdf_data
181
+ except Exception as e:
182
+ # Handle errors and ensure temporary files are deleted
183
+ if os.path.exists(temp_html_path):
184
+ os.unlink(temp_html_path)
185
+ raise e
186
+
187
+ def send_report_via_email(report_markdown, recipient_email):
188
+ """Send market report via email"""
189
+ try:
190
+ # Generate PDF from markdown
191
+ pdf_data = _generate_pdf_from_markdown(report_markdown)
192
+
193
+ # Create message
194
+ message = MIMEMultipart()
195
+ message["From"] = SENDER_EMAIL
196
+ message["To"] = recipient_email
197
+ message["Subject"] = f"AI Financial Dashboard - Daily Market Report {datetime.now().strftime('%d/%m/%Y')}"
198
+
199
+ # Add content with UTF-8 encoding
200
+ body = """
201
+ Dear User,
202
+
203
+ Attached is today's financial market report, automatically generated by AI Financial Dashboard.
204
+
205
+ Best regards,
206
+ AI Financial Dashboard Team
207
+ """
208
+ message.attach(MIMEText(body, "plain", "utf-8"))
209
+
210
+ # Attach PDF file
211
+ attachment = MIMEApplication(pdf_data, _subtype="pdf")
212
+ attachment.add_header(
213
+ "Content-Disposition", "attachment",
214
+ filename=f"Market_Report_{datetime.now().strftime('%Y%m%d')}.pdf"
215
+ )
216
+ message.attach(attachment)
217
+
218
+ # Connect to SMTP server and send email
219
+ with smtplib.SMTP_SSL("smtp.gmail.com", 465) as server:
220
+ server.login(SENDER_EMAIL, SENDER_APP_PASSWORD)
221
+ server.send_message(message)
222
+
223
+ return True, "Email sent successfully!"
224
+
225
+ except Exception as e:
226
+ return False, f"Error sending email: {str(e)}"
modules/news_pipeline.py ADDED
@@ -0,0 +1,539 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import asyncio
3
+ import aiohttp
4
+ import time
5
+ import re
6
+ import random
7
+ from datetime import datetime, timedelta
8
+ import google.generativeai as genai
9
+ from dotenv import load_dotenv
10
+ from concurrent.futures import ThreadPoolExecutor
11
+ from groq import Groq, AsyncGroq
12
+ from threading import Lock
13
+
14
+ # Tải biến môi trường
15
+ load_dotenv()
16
+
17
+ # Cấu hình Gemini AI
18
+ GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
19
+ genai.configure(api_key=GEMINI_API_KEY)
20
+ MODEL_NAME = os.getenv("GEMINI_MODEL", "gemini-2.5-flash")
21
+
22
+ # Cấu hình Groq AI
23
+ GROQ_API_KEYS_STR = os.getenv("GROQ_API_KEY", "")
24
+ GROQ_MODEL = "meta-llama/llama-4-scout-17b-16e-instruct" # Model Llama4 qua Groq
25
+
26
+ # Quản lý nhiều API keys cho Groq
27
+ class GroqClientManager:
28
+ """Quản lý pool các Groq clients với logic round-robin"""
29
+
30
+ def __init__(self, api_keys_str):
31
+ # Tách chuỗi API keys (key1,key2,key3) thành list
32
+ self.api_keys = [key.strip() for key in api_keys_str.split(",") if key.strip()]
33
+ if not self.api_keys:
34
+ raise ValueError("Không tìm thấy API key hợp lệ cho Groq")
35
+
36
+ print(f"Khởi tạo {len(self.api_keys)} Groq API clients")
37
+
38
+ # Tạo pool của các client
39
+ self.clients = [AsyncGroq(api_key=key) for key in self.api_keys]
40
+ self.current_index = 0
41
+ self.lock = Lock()
42
+
43
+ # Thống kê sử dụng
44
+ self.usage_stats = {i: 0 for i in range(len(self.api_keys))}
45
+
46
+ def get_next_client(self):
47
+ """Lấy client tiếp theo theo cơ chế round-robin"""
48
+ with self.lock:
49
+ client = self.clients[self.current_index]
50
+ # Cập nhật thống kê
51
+ self.usage_stats[self.current_index] += 1
52
+ # Di chuyển đến key tiếp theo
53
+ self.current_index = (self.current_index + 1) % len(self.clients)
54
+ return client
55
+
56
+ def print_usage_stats(self):
57
+ """In thống kê sử dụng của từng API key"""
58
+ with self.lock:
59
+ print("\n--- Thống kê sử dụng Groq API keys ---")
60
+ total_calls = sum(self.usage_stats.values())
61
+ for idx, count in self.usage_stats.items():
62
+ key_preview = f"{self.api_keys[idx][:8]}..." if len(self.api_keys[idx]) > 10 else self.api_keys[idx]
63
+ percentage = (count / total_calls * 100) if total_calls > 0 else 0
64
+ print(f"Key {idx+1} ({key_preview}): {count} lần gọi ({percentage:.1f}%)")
65
+ print(f"Tổng số lần gọi API: {total_calls}")
66
+ print("---------------------------------------\n")
67
+
68
+ # Khởi tạo singleton manager
69
+ groq_client_manager = GroqClientManager(GROQ_API_KEYS_STR)
70
+
71
+ # Giữ lại một client đơn cho compatibility
72
+ groq_client = Groq(api_key=groq_client_manager.api_keys[0] if groq_client_manager.api_keys else "")
73
+
74
+ MAX_ARTICLES = 30 # 5 for testing, 30 for production
75
+
76
+ # Các API keys
77
+ NEWS_API_KEY = os.getenv("NEWS_API_KEY")
78
+ MARKETAUX_API_KEY = os.getenv("MARKETAUX_API_KEY")
79
+ ALPHA_VANTAGE_API_KEY = os.getenv("ALPHA_VANTAGE_API_KEY")
80
+
81
+ # Cấu hình kiểm soát tốc độ gọi API
82
+ MAX_REQUESTS_PER_MINUTE = 10 # Để an toàn, giữ dưới 10
83
+ BATCH_SIZE = 10 # Số lượng bài báo được xử lý cùng lúc
84
+ DELAY_BETWEEN_BATCHES = 10 # Thời gian chờ giữa các batch (giây)
85
+ RETRY_DELAY_BASE = 5 # Thời gian cơ sở cho retry (giây)
86
+ MAX_RETRIES = 3 # Số lần thử lại tối đa
87
+
88
+ # Cấu hình riêng cho Groq API
89
+ GROQ_BATCH_SIZE = 10 # Số lượng bài báo được xử lý cùng lúc khi dùng Groq (cao hơn Gemini)
90
+ GROQ_DELAY_BETWEEN_REQUESTS = 2 # Thời gian chờ giữa các request riêng lẻ với Groq (giây)
91
+ GROQ_DELAY_BETWEEN_BATCHES = 5 # Thời gian chờ giữa các batch với Groq (giây)
92
+
93
+ class NewsArticle:
94
+ """Lớp tiêu chuẩn hóa cho các bài báo từ các nguồn khác nhau"""
95
+ def __init__(self, title, description, content, source_name, url, published_at):
96
+ self.title = title
97
+ self.description = description
98
+ self.content = content
99
+ self.source_name = source_name
100
+ self.url = url
101
+ self.published_at = published_at
102
+
103
+ def __str__(self):
104
+ return f"{self.title} ({self.source_name})"
105
+
106
+ def __eq__(self, other):
107
+ if not isinstance(other, NewsArticle):
108
+ return False
109
+ # So sánh bằng URL hoặc tiêu đề
110
+ return self.url == other.url or self.title == other.title
111
+
112
+ def __hash__(self):
113
+ # Sử dụng URL làm hash
114
+ return hash(self.url)
115
+
116
+ async def fetch_from_newsapi():
117
+ """Lấy tin tức từ NewsAPI"""
118
+ url = "https://newsapi.org/v2/everything"
119
+ yesterday = datetime.now() - timedelta(days=1)
120
+ yesterday_str = yesterday.strftime('%Y-%m-%d')
121
+
122
+ params = {
123
+ 'q': 'finance OR economy OR stock market OR investing',
124
+ 'from': yesterday_str,
125
+ 'language': 'en',
126
+ 'sortBy': 'publishedAt',
127
+ 'pageSize': 50,
128
+ 'apiKey': NEWS_API_KEY
129
+ }
130
+
131
+ async with aiohttp.ClientSession() as session:
132
+ async with session.get(url, params=params) as response:
133
+ if response.status == 200:
134
+ data = await response.json()
135
+ articles = []
136
+ if data.get('status') == 'ok' and 'articles' in data:
137
+ for article in data['articles']:
138
+ articles.append(
139
+ NewsArticle(
140
+ title=article.get('title', ''),
141
+ description=article.get('description', ''),
142
+ content=article.get('content', ''),
143
+ source_name=article.get('source', {}).get('name', 'Unknown'),
144
+ url=article.get('url', ''),
145
+ published_at=article.get('publishedAt', '')
146
+ )
147
+ )
148
+ return articles
149
+ return []
150
+
151
+ async def fetch_from_marketaux():
152
+ """Lấy tin tức từ Marketaux API"""
153
+ url = "https://api.marketaux.com/v1/news/all"
154
+
155
+ params = {
156
+ 'symbols': 'AAPL,MSFT,TSLA,AMZN,NVDA,META', # Một số mã chứng khoán phổ biến
157
+ 'filter_entities': 'true',
158
+ 'language': 'en',
159
+ 'published_after': (datetime.now() - timedelta(days=1)).strftime('%Y-%m-%dT%H:%M'),
160
+ 'limit': 50,
161
+ 'api_token': MARKETAUX_API_KEY
162
+ }
163
+
164
+ async with aiohttp.ClientSession() as session:
165
+ async with session.get(url, params=params) as response:
166
+ if response.status == 200:
167
+ data = await response.json()
168
+ articles = []
169
+ if 'data' in data:
170
+ for article in data['data']:
171
+ articles.append(
172
+ NewsArticle(
173
+ title=article.get('title', ''),
174
+ description=article.get('description', ''),
175
+ content=article.get('snippet', ''),
176
+ source_name=article.get('source', ''),
177
+ url=article.get('url', ''),
178
+ published_at=article.get('published_at', '')
179
+ )
180
+ )
181
+ return articles
182
+ return []
183
+
184
+ def _normalize_and_deduplicate(articles_list):
185
+ """Chuẩn hóa và loại bỏ các bài báo trùng lặp"""
186
+ # Loại bỏ trùng lặp bằng cách chuyển thành set
187
+ unique_articles = set()
188
+
189
+ for article in articles_list:
190
+ if article.title and article.content: # Bỏ qua bài viết không có tiêu đề hoặc nội dung
191
+ # Loại bỏ các bài quảng cáo và không liên quan
192
+ skip_keywords = ["advertisement", "sponsored", "promotion"]
193
+ if not any(keyword in article.title.lower() for keyword in skip_keywords):
194
+ unique_articles.add(article)
195
+
196
+ # Sắp xếp theo thời gian xuất bản (mới nhất trước)
197
+ sorted_articles = sorted(
198
+ unique_articles,
199
+ key=lambda x: datetime.fromisoformat(x.published_at.replace('Z', '+00:00').replace('T', ' ')),
200
+ reverse=True
201
+ )
202
+
203
+ # Giới hạn số lượng bài viết
204
+ max_articles = min(len(sorted_articles), MAX_ARTICLES) # Giảm số lượng bài từ 30 xuống 20
205
+ return sorted_articles[:max_articles]
206
+
207
+ async def _call_ai_with_retry(prompt, model, retries=MAX_RETRIES):
208
+ """Gọi Gemini API với cơ chế retry và exponential backoff"""
209
+ attempt = 0
210
+ last_exception = None
211
+
212
+ while attempt <= retries:
213
+ try:
214
+ # Thêm jitter vào delay để tránh đồng bộ hóa các yêu cầu
215
+ jitter = random.uniform(0.5, 1.5)
216
+
217
+ if attempt > 0:
218
+ # Exponential backoff với jitter
219
+ delay = (RETRY_DELAY_BASE * (2 ** (attempt - 1))) * jitter
220
+ print(f"Retry lần {attempt}, đợi {delay:.2f} giây...")
221
+ await asyncio.sleep(delay)
222
+
223
+ response = await model.generate_content_async(prompt)
224
+ return response.text
225
+
226
+ except Exception as e:
227
+ last_exception = e
228
+ print(f"Lỗi khi gọi AI (lần {attempt+1}/{retries+1}): {str(e)}")
229
+
230
+ # Nếu đây là lỗi quota, thêm thời gian chờ dài hơn
231
+ if "429" in str(e) or "quota" in str(e).lower():
232
+ # Thêm thời gian chờ dài hơn cho lỗi quota (60-90 giây)
233
+ quota_delay = random.uniform(60, 90)
234
+ print(f"Phát hiện lỗi quota, đợi {quota_delay:.2f} giây...")
235
+ await asyncio.sleep(quota_delay)
236
+
237
+ attempt += 1
238
+
239
+ # Nếu đã hết số lần thử lại, ném ngoại lệ
240
+ if last_exception:
241
+ raise last_exception
242
+ else:
243
+ raise Exception("Không thể gọi API Gemini sau nhiều lần thử lại")
244
+
245
+ async def _call_groq_with_retry(prompt, retries=MAX_RETRIES):
246
+ """Gọi Groq API với cơ chế retry và round-robin API keys"""
247
+ attempt = 0
248
+ last_exception = None
249
+
250
+ while attempt <= retries:
251
+ try:
252
+ # Thêm jitter vào delay để tránh đồng bộ hóa các yêu cầu
253
+ jitter = random.uniform(0.5, 1.5)
254
+
255
+ if attempt > 0:
256
+ # Exponential backoff với jitter
257
+ delay = (RETRY_DELAY_BASE * (2 ** (attempt - 1))) * jitter
258
+ print(f"Retry Groq lần {attempt}, đợi {delay:.2f} giây...")
259
+ await asyncio.sleep(delay)
260
+
261
+ # Lấy client tiếp theo từ round-robin pool
262
+ client = groq_client_manager.get_next_client()
263
+
264
+ response = await client.chat.completions.create(
265
+ model=GROQ_MODEL,
266
+ messages=[
267
+ {"role": "user", "content": prompt}
268
+ ],
269
+ temperature=0.3, # Thấp hơn để tóm tắt chính xác
270
+ max_tokens=500, # Giới hạn độ dài tóm tắt
271
+ top_p=0.95,
272
+ stream=False
273
+ )
274
+
275
+ return response.choices[0].message.content
276
+
277
+ except Exception as e:
278
+ last_exception = e
279
+ print(f"Lỗi khi gọi Groq API (lần {attempt+1}/{retries+1}): {str(e)}")
280
+
281
+ # Nếu đây là lỗi rate limit hoặc quota
282
+ if "429" in str(e) or "rate" in str(e).lower() or "limit" in str(e).lower():
283
+ # Thêm thời gian chờ dài hơn
284
+ rate_limit_delay = random.uniform(30, 45)
285
+ print(f"Phát hiện lỗi rate limit, đợi {rate_limit_delay:.2f} giây...")
286
+ await asyncio.sleep(rate_limit_delay)
287
+
288
+ attempt += 1
289
+
290
+ # Nếu đã hết số lần thử lại, ném ngoại lệ
291
+ if last_exception:
292
+ raise last_exception
293
+ else:
294
+ raise Exception("Không thể gọi API Groq sau nhiều lần thử lại")
295
+
296
+ async def _summarize_article_with_groq(article):
297
+ """Tạo tóm tắt cho một bài báo sử dụng Groq API"""
298
+ prompt = f"""
299
+ Summarize the most important information from this financial article in a concise paragraph (no more than 2-3 sentences).
300
+ Focus on key events, figures, trends, or information valuable to investors.
301
+ Provide only the summary without any introduction or conclusion.
302
+
303
+ TITLE: {article.title}
304
+ DESCRIPTION: {article.description}
305
+ CONTENT: {article.content}
306
+ SOURCE: {article.source_name}
307
+ """
308
+
309
+ try:
310
+ # Sử dụng hàm gọi Groq API có retry
311
+ summary = await _call_groq_with_retry(prompt)
312
+
313
+ # Loại bỏ các ký tự đặc biệt và chuẩn hóa
314
+ summary = re.sub(r'[\n\r]+', ' ', summary)
315
+ summary = re.sub(r'\s{2,}', ' ', summary).strip()
316
+
317
+ return {
318
+ 'title': article.title,
319
+ 'source': article.source_name,
320
+ 'summary': summary,
321
+ 'url': article.url
322
+ }
323
+ except Exception as e:
324
+ print(f"Lỗi khi tóm tắt bài báo với Groq: {str(e)}")
325
+ return {
326
+ 'title': article.title,
327
+ 'source': article.source_name,
328
+ 'summary': "Unable to summarize this article due to API limitations.",
329
+ 'url': article.url
330
+ }
331
+
332
+ async def _summarize_article(article, model):
333
+ """Tạo tóm tắt cho một bài báo với Gemini (giữ lại để dự phòng)"""
334
+ prompt = f"""
335
+ Summarize the most important information from this financial article in a concise paragraph (no more than 2-3 sentences).
336
+ Focus on key events, figures, trends, or information valuable to investors.
337
+ Provide only the summary without any introduction or conclusion.
338
+
339
+ TITLE: {article.title}
340
+ DESCRIPTION: {article.description}
341
+ CONTENT: {article.content}
342
+ SOURCE: {article.source_name}
343
+ URL: {article.url}
344
+ """
345
+
346
+ try:
347
+ # Sử dụng hàm gọi API có retry
348
+ summary = await _call_ai_with_retry(prompt, model)
349
+
350
+ # Loại bỏ các ký tự đặc biệt và chuẩn hóa
351
+ summary = re.sub(r'[\n\r]+', ' ', summary)
352
+ summary = re.sub(r'\s{2,}', ' ', summary).strip()
353
+
354
+ return {
355
+ 'title': article.title,
356
+ 'source': article.source_name,
357
+ 'summary': summary,
358
+ 'url': article.url
359
+ }
360
+ except Exception as e:
361
+ print(f"Lỗi khi tóm tắt bài báo với Gemini: {str(e)}")
362
+ return {
363
+ 'title': article.title,
364
+ 'source': article.source_name,
365
+ 'summary': "Unable to summarize this article due to API limitations.",
366
+ 'url': article.url
367
+ }
368
+
369
+ async def _summarize_articles_with_groq(articles):
370
+ """Tóm tắt các bài báo bằng Groq API"""
371
+ all_summaries = []
372
+
373
+ # Chia bài viết thành các batch nhỏ
374
+ for i in range(0, len(articles), GROQ_BATCH_SIZE):
375
+ batch = articles[i:i+GROQ_BATCH_SIZE]
376
+ print(f"Đang xử lý batch {i//GROQ_BATCH_SIZE + 1}/{(len(articles) + GROQ_BATCH_SIZE - 1)//GROQ_BATCH_SIZE} ({len(batch)} bài) với Groq")
377
+
378
+ # Tạo danh sách các coroutines để xử lý song song
379
+ tasks = [_summarize_article_with_groq(article) for article in batch]
380
+
381
+ # Chờ tất cả các task hoàn thành
382
+ batch_summaries = await asyncio.gather(*tasks)
383
+ all_summaries.extend(batch_summaries)
384
+
385
+ # Nếu còn batch tiếp theo, đợi để tránh vượt quá quota
386
+ if i + GROQ_BATCH_SIZE < len(articles):
387
+ wait_time = random.uniform(GROQ_DELAY_BETWEEN_BATCHES * 0.9, GROQ_DELAY_BETWEEN_BATCHES * 1.1)
388
+ print(f"Hoàn thành batch. Đang đợi {wait_time:.2f}s trước batch tiếp theo để tránh quá tải quota...")
389
+ await asyncio.sleep(wait_time)
390
+
391
+ # In thống kê sử dụng API keys
392
+ groq_client_manager.print_usage_stats()
393
+
394
+ return all_summaries
395
+
396
+ async def _summarize_articles_in_batches(articles, model):
397
+ """Tóm tắt các bài báo theo batch với Gemini API (giữ lại để dự phòng)"""
398
+ all_summaries = []
399
+
400
+ # Chia bài viết thành các batch nhỏ
401
+ for i in range(0, len(articles), BATCH_SIZE):
402
+ batch = articles[i:i+BATCH_SIZE]
403
+ print(f"Đang xử lý batch {i//BATCH_SIZE + 1}/{(len(articles) + BATCH_SIZE - 1)//BATCH_SIZE} ({len(batch)} bài)")
404
+
405
+ # Xử lý các bài trong batch một cách tuần tự để tránh quá tải API
406
+ batch_summaries = []
407
+ for article in batch:
408
+ # Thêm jitter vào delay để tránh đồng bộ hóa các yêu cầu
409
+ delay = random.uniform(1.0, 3.0)
410
+ await asyncio.sleep(delay)
411
+
412
+ summary = await _summarize_article(article, model)
413
+ batch_summaries.append(summary)
414
+
415
+ all_summaries.extend(batch_summaries)
416
+
417
+ # Nếu còn batch tiếp theo, đợi để tránh vượt quá quota
418
+ if i + BATCH_SIZE < len(articles):
419
+ wait_time = random.uniform(DELAY_BETWEEN_BATCHES * 0.9, DELAY_BETWEEN_BATCHES * 1.1)
420
+ print(f"Hoàn thành batch. Đang đợi {wait_time:.2f}s trước batch tiếp theo để tránh quá tải quota...")
421
+ await asyncio.sleep(wait_time)
422
+
423
+ return all_summaries
424
+
425
+ async def _synthesize_newsletter(summaries, model):
426
+ """Reduce phase: Tổng hợp các tóm tắt thành một bản tin hoàn chỉnh sử dụng Gemini API"""
427
+ # Giới hạn số lượng tóm tắt để đảm bảo không vượt quá token limit
428
+ # if len(summaries) > 15:
429
+ # print(f"Giới hạn số lượng tóm tắt từ {len(summaries)} xuống 15 để phù hợp với giới hạn token")
430
+ # summaries = summaries[:15]
431
+
432
+ # Chuẩn bị dữ liệu đầu vào cho prompt
433
+ summaries_text = ""
434
+ for i, s in enumerate(summaries, 1):
435
+ summaries_text += f"{i}. **{s['title']}** ({s['source']}): {s['summary']} [Link]({s['url']})\n\n"
436
+
437
+ # Prompt để tạo bản tin
438
+ prompt = f"""
439
+ Below are summaries from {len(summaries)} financial articles published in the last 24 hours:
440
+
441
+ {summaries_text}
442
+
443
+ Write a comprehensive market report based on these summaries. The report should:
444
+
445
+ 1. Have an overall headline for the entire report
446
+ 2. Be organized into clear sections by topic, such as:
447
+ - Macroeconomic Developments
448
+ - Corporate News
449
+ - Stock Market Performance
450
+ - Cryptocurrency and Fintech
451
+ - Expert Opinions and Forecasts
452
+ 3. Each section should have at least 3-5 concise news points, with sources cited
453
+ 4. End with a brief conclusion about the overall market trends
454
+
455
+ Format the report in Markdown. Ensure clear numbering and proper Markdown syntax.
456
+ """
457
+
458
+ try:
459
+ # Thêm jitter vào delay trước khi gọi API tổng hợp
460
+ await asyncio.sleep(random.uniform(3.0, 5.0))
461
+
462
+ # Sử dụng hàm gọi API có retry
463
+ newsletter = await _call_ai_with_retry(prompt, model)
464
+
465
+ # Thêm phần thông tin về ngày tạo
466
+ today = datetime.now().strftime("%d/%m/%Y")
467
+ newsletter = f"# DAILY MARKET REPORT - {today}\n\n" + newsletter
468
+
469
+ # Thêm phần footer
470
+ footer = """
471
+ ---
472
+
473
+ *This report was automatically generated by AI Financial Dashboard based on data from multiple reliable financial news sources.*
474
+ *Note: This is not investment advice.*
475
+ """
476
+ newsletter += footer
477
+
478
+ return newsletter
479
+ except Exception as e:
480
+ print(f"Lỗi khi tạo bản tin: {str(e)}")
481
+ return """# DAILY MARKET REPORT
482
+
483
+ We're sorry, but the market report could not be automatically generated due to API limitations. Please try again later.
484
+
485
+ ---
486
+
487
+ *Note: The system has collected data but could not generate the report due to API quota limitations.*
488
+ """
489
+
490
+ async def run_news_summary_pipeline():
491
+ """Hàm chính để chạy toàn bộ pipeline tạo bản tin"""
492
+ start_time = time.time()
493
+
494
+ # 1. Khởi tạo model
495
+ model = genai.GenerativeModel(MODEL_NAME)
496
+
497
+ # 2. Thu thập tin tức từ nhiều nguồn song song
498
+ print("Đang thu thập tin tức từ các nguồn...")
499
+ tasks = [
500
+ fetch_from_newsapi(),
501
+ fetch_from_marketaux()
502
+ ]
503
+
504
+ # Chờ tất cả các task hoàn thành
505
+ results = await asyncio.gather(*tasks)
506
+
507
+ # Gộp kết quả từ các nguồn
508
+ all_articles = []
509
+ for articles in results:
510
+ if articles:
511
+ all_articles.extend(articles)
512
+
513
+ print(f"Đã thu thập {len(all_articles)} bài báo từ các nguồn.")
514
+
515
+ # 3. Chuẩn hóa và loại bỏ trùng lặp
516
+ articles = _normalize_and_deduplicate(all_articles)
517
+ print(f"Sau khi lọc: {len(articles)} bài báo duy nhất.")
518
+
519
+ # 4. Tóm tắt từng bài báo sử dụng Groq API (nhanh hơn, quota cao hơn)
520
+ print("Bắt đầu tóm tắt các bài báo với Groq API...")
521
+ try:
522
+ summaries = await _summarize_articles_with_groq(articles)
523
+ print(f"Đã tóm tắt {len(summaries)} bài báo với Groq API.")
524
+ except Exception as e:
525
+ print(f"Lỗi khi sử dụng Groq API: {str(e)}. Chuyển sang sử dụng Gemini API...")
526
+ # Fallback sang Gemini nếu có lỗi với Groq
527
+ summaries = await _summarize_articles_in_batches(articles, model)
528
+ print(f"Đã tóm tắt {len(summaries)} bài báo với Gemini API.")
529
+
530
+ # 5. Tổng hợp bản tin sử dụng Gemini API (tốt hơn với việc viết nội dung dài)
531
+ print("Đang tạo bản tin tổng hợp với Gemini API...")
532
+ newsletter = await _synthesize_newsletter(summaries, model)
533
+
534
+ # Thống kê thời gian thực hiện
535
+ end_time = time.time()
536
+ duration = end_time - start_time
537
+ print(f"Pipeline hoàn tất trong {duration:.2f} giây.")
538
+
539
+ return newsletter
pages/Daily_News_Report.py ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import streamlit as st
3
+ import asyncio
4
+ from datetime import datetime
5
+ import sys
6
+ import traceback
7
+ import re
8
+ from email_validator import validate_email, EmailNotValidError
9
+
10
+ # Add project root to path for imports
11
+ sys.path.append(os.path.dirname(os.path.dirname(__file__)))
12
+
13
+ # Import required modules
14
+ from modules.news_pipeline import run_news_summary_pipeline
15
+ from modules.email_sender import send_report_via_email
16
+
17
+ # Page setup
18
+ st.set_page_config(
19
+ page_title="Daily Market News Report",
20
+ page_icon="📰",
21
+ layout="wide",
22
+ initial_sidebar_state="expanded"
23
+ )
24
+
25
+ # Initialize session state
26
+ if "news_report_initialized" not in st.session_state:
27
+ st.session_state.news_report_initialized = True
28
+ if "newsletter_content" not in st.session_state:
29
+ st.session_state.newsletter_content = None
30
+ if "last_generated" not in st.session_state:
31
+ st.session_state.last_generated = None
32
+
33
+ # Create main container
34
+ main_container = st.container()
35
+
36
+ with main_container:
37
+ # Page title
38
+ st.title("📰 Daily Market News Report")
39
+
40
+ # Description
41
+ st.markdown("""
42
+ This feature uses AI to automatically scan, analyze, and summarize the latest financial news from multiple reliable sources.
43
+ The newsletter is updated daily, helping you quickly grasp market developments.
44
+ """)
45
+
46
+ # Create columns for generate button and timestamp
47
+ col1, col2 = st.columns([4, 8])
48
+
49
+ with col1:
50
+ # Button to create a new newsletter
51
+ if st.button("🔄 Generate Today's Report", use_container_width=True):
52
+ # Show spinner while processing
53
+ with st.spinner("AI is analyzing news from multiple sources..."):
54
+ try:
55
+ # Call newsletter pipeline
56
+ newsletter = asyncio.run(run_news_summary_pipeline())
57
+
58
+ # Save results to session state
59
+ st.session_state.newsletter_content = newsletter
60
+ st.session_state.last_generated = datetime.now()
61
+
62
+ # Display success message
63
+ st.success("Report successfully generated!")
64
+ except Exception as e:
65
+ st.error(f"An error occurred: {str(e)}")
66
+ traceback.print_exc()
67
+
68
+ with col2:
69
+ # Display last generation time
70
+ if st.session_state.last_generated:
71
+ st.info(f"Last updated: {st.session_state.last_generated.strftime('%d/%m/%Y %H:%M')}")
72
+
73
+ # Display newsletter if available
74
+ if st.session_state.newsletter_content:
75
+ # Create tabs to display newsletter and email form
76
+ tab1, tab2 = st.tabs(["Report", "Email Report"])
77
+
78
+ with tab1:
79
+ # Display newsletter content
80
+ st.markdown(st.session_state.newsletter_content)
81
+
82
+ with tab2:
83
+ # Form to send email
84
+ st.markdown("### Send Report via Email")
85
+
86
+ with st.form(key="email_form"):
87
+ # Email input
88
+ email = st.text_input("Enter email address to receive the report")
89
+
90
+ # Submit button
91
+ submit_button = st.form_submit_button(label="📩 Send Report via Email")
92
+
93
+ if submit_button:
94
+ # Validate email
95
+ if not email:
96
+ st.error("Please enter an email address.")
97
+ else:
98
+ try:
99
+ # Validate email format
100
+ validate_email(email)
101
+
102
+ # Show spinner while sending
103
+ with st.spinner("Sending email..."):
104
+ # Call send email function
105
+ success, message = send_report_via_email(
106
+ st.session_state.newsletter_content,
107
+ email
108
+ )
109
+
110
+ # Display result
111
+ if success:
112
+ st.success(message)
113
+ else:
114
+ st.error(message)
115
+ except EmailNotValidError:
116
+ st.error("Invalid email address.")
117
+ except Exception as e:
118
+ st.error(f"An error occurred: {str(e)}")
119
+
120
+ # Add privacy note
121
+ st.info("📝 **Note:** Your email is only used to send this report and is not stored.")
122
+ else:
123
+ # Display note if no newsletter available
124
+ st.info("👆 Click 'Generate Today's Report' to start analyzing the news.")
125
+
126
+ # Add explanation about process
127
+ with st.expander("How it works"):
128
+ st.markdown("""
129
+ ### Report Generation Process:
130
+
131
+ 1. **Data Collection**: The system automatically scans the latest financial news from multiple reliable sources.
132
+
133
+ 2. **Analysis & Filtering**: AI removes duplicate and irrelevant news, keeping only the most important information.
134
+
135
+ 3. **Summarization**: Each article is summarized into key points, helping you quickly grasp the information.
136
+
137
+ 4. **Synthesis**: AI organizes information by topics and writes it into a well-structured report.
138
+
139
+ 5. **Display & Share**: The final report is displayed on the web and can be sent via email upon request.
140
+ """)
pages/{stock_report.py → Stock_Analysis.py} RENAMED
File without changes
pages/{chat_app.py → Stock_Chatbot.py} RENAMED
@@ -13,6 +13,7 @@ from datetime import datetime
13
 
14
  # --- 1. INITIAL CONFIGURATION & STATE INITIALIZATION ---
15
  load_dotenv()
 
16
 
17
  # Set page config consistent with other pages
18
  st.set_page_config(
@@ -159,7 +160,7 @@ def get_model_and_tools():
159
  get_ts_func = glm.FunctionDeclaration(name="get_smart_time_series", description="Get price history data after knowing the official stock symbol.", parameters=glm.Schema(type=glm.Type.OBJECT, properties={'symbol': glm.Schema(type=glm.Type.STRING), 'time_period': glm.Schema(type=glm.Type.STRING, enum=["intraday", "1_week", "1_month", "6_months", "1_year"])}, required=['symbol', 'time_period']))
160
  currency_func = glm.FunctionDeclaration(name="perform_currency_conversion", description="Convert currency after knowing the 3-letter code of source/target currency pair, e.g., USD/VND, JPY/EUR", parameters=glm.Schema(type=glm.Type.OBJECT, properties={'amount': glm.Schema(type=glm.Type.NUMBER), 'symbol': glm.Schema(type=glm.Type.STRING)}, required=['amount', 'symbol']))
161
  finance_tool = glm.Tool(function_declarations=[find_stock_func, get_ts_func, currency_func])
162
- model = genai.GenerativeModel(model_name="gemini-2.5-flash", tools=[finance_tool], system_instruction=SYSTEM_INSTRUCTION)
163
  return model
164
  model = get_model_and_tools()
165
  if st.session_state.chat_session is None:
 
13
 
14
  # --- 1. INITIAL CONFIGURATION & STATE INITIALIZATION ---
15
  load_dotenv()
16
+ MODEL_NAME = os.getenv("GEMINI_MODEL", "gemini-2.5-flash")
17
 
18
  # Set page config consistent with other pages
19
  st.set_page_config(
 
160
  get_ts_func = glm.FunctionDeclaration(name="get_smart_time_series", description="Get price history data after knowing the official stock symbol.", parameters=glm.Schema(type=glm.Type.OBJECT, properties={'symbol': glm.Schema(type=glm.Type.STRING), 'time_period': glm.Schema(type=glm.Type.STRING, enum=["intraday", "1_week", "1_month", "6_months", "1_year"])}, required=['symbol', 'time_period']))
161
  currency_func = glm.FunctionDeclaration(name="perform_currency_conversion", description="Convert currency after knowing the 3-letter code of source/target currency pair, e.g., USD/VND, JPY/EUR", parameters=glm.Schema(type=glm.Type.OBJECT, properties={'amount': glm.Schema(type=glm.Type.NUMBER), 'symbol': glm.Schema(type=glm.Type.STRING)}, required=['amount', 'symbol']))
162
  finance_tool = glm.Tool(function_declarations=[find_stock_func, get_ts_func, currency_func])
163
+ model = genai.GenerativeModel(model_name=MODEL_NAME, tools=[finance_tool], system_instruction=SYSTEM_INSTRUCTION)
164
  return model
165
  model = get_model_and_tools()
166
  if st.session_state.chat_session is None:
requirements.txt CHANGED
@@ -16,8 +16,13 @@ click==8.2.1
16
  comm==0.2.3
17
  cssselect2==0.8.0
18
  decorator==5.2.1
 
 
 
 
19
  executing==2.2.0
20
  fonttools==4.59.0
 
21
  frozenlist==1.7.0
22
  gitdb==4.0.12
23
  GitPython==3.1.45
@@ -29,6 +34,7 @@ google-auth-httplib2==0.2.0
29
  google-genai==1.27.0
30
  google-generativeai==0.8.5
31
  googleapis-common-protos==1.70.0
 
32
  grpcio==1.74.0
33
  grpcio-status==1.71.2
34
  h11==0.16.0
 
16
  comm==0.2.3
17
  cssselect2==0.8.0
18
  decorator==5.2.1
19
+ defusedxml==0.7.1
20
+ distro==1.9.0
21
+ dnspython==2.7.0
22
+ email_validator==2.2.0
23
  executing==2.2.0
24
  fonttools==4.59.0
25
+ fpdf==1.7.2
26
  frozenlist==1.7.0
27
  gitdb==4.0.12
28
  GitPython==3.1.45
 
34
  google-genai==1.27.0
35
  google-generativeai==0.8.5
36
  googleapis-common-protos==1.70.0
37
+ groq==0.30.0
38
  grpcio==1.74.0
39
  grpcio-status==1.71.2
40
  h11==0.16.0
test.py ADDED
@@ -0,0 +1,235 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import smtplib
3
+ import tempfile
4
+ from email.mime.multipart import MIMEMultipart
5
+ from email.mime.text import MIMEText
6
+ from email.mime.application import MIMEApplication
7
+ from dotenv import load_dotenv
8
+ from markdown_it import MarkdownIt
9
+ from datetime import datetime
10
+ import weasyprint
11
+
12
+ # Tải biến môi trường
13
+ load_dotenv()
14
+
15
+ # Thông tin email từ biến môi trường
16
+ SENDER_EMAIL = os.getenv("SENDER_EMAIL")
17
+ SENDER_APP_PASSWORD = os.getenv("SENDER_APP_PASSWORD")
18
+
19
+ def _markdown_to_html(markdown_string):
20
+ """Chuyển đổi Markdown thành HTML"""
21
+ md = MarkdownIt()
22
+ html_content = md.render(markdown_string)
23
+
24
+ # Định dạng ngày hiện tại
25
+ current_date = datetime.now().strftime("%d/%m/%Y")
26
+
27
+ # Tạo HTML hoàn chỉnh với CSS để định dạng đẹp
28
+ full_html = f"""
29
+ <!DOCTYPE html>
30
+ <html>
31
+ <head>
32
+ <meta charset="UTF-8">
33
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
34
+ <title>Bản tin Thị trường Ngày {current_date}</title>
35
+ <style>
36
+ @page {{
37
+ size: A4;
38
+ margin: 2cm;
39
+ }}
40
+ body {{
41
+ font-family: Arial, Helvetica, sans-serif;
42
+ line-height: 1.5;
43
+ color: #333;
44
+ max-width: 800px;
45
+ margin: 0 auto;
46
+ }}
47
+ .report-header {{
48
+ text-align: center;
49
+ margin-bottom: 30px;
50
+ }}
51
+ .report-date {{
52
+ font-style: italic;
53
+ color: #666;
54
+ margin-bottom: 10px;
55
+ }}
56
+ .report-title {{
57
+ font-size: 24pt;
58
+ margin-bottom: 5px;
59
+ color: #2c3e50;
60
+ }}
61
+ .report-subtitle {{
62
+ font-size: 14pt;
63
+ color: #7f8c8d;
64
+ margin-top: 0;
65
+ }}
66
+ .report-body {{
67
+ text-align: justify;
68
+ }}
69
+ h1, h2, h3, h4, h5, h6 {{
70
+ color: #2c3e50;
71
+ margin-top: 20px;
72
+ }}
73
+ h1 {{ font-size: 20pt; }}
74
+ h2 {{ font-size: 18pt; }}
75
+ h3 {{ font-size: 16pt; }}
76
+ h4 {{ font-size: 14pt; }}
77
+ h5 {{ font-size: 12pt; }}
78
+ h6 {{ font-size: 10pt; }}
79
+
80
+ p {{
81
+ margin-bottom: 10px;
82
+ }}
83
+
84
+ a {{
85
+ color: #3498db;
86
+ text-decoration: none;
87
+ }}
88
+
89
+ a:hover {{
90
+ text-decoration: underline;
91
+ }}
92
+
93
+ ul, ol {{
94
+ margin: 10px 0 10px 20px;
95
+ }}
96
+
97
+ li {{
98
+ margin-bottom: 5px;
99
+ }}
100
+
101
+ blockquote {{
102
+ border-left: 4px solid #eee;
103
+ padding-left: 10px;
104
+ margin-left: 0;
105
+ color: #777;
106
+ }}
107
+
108
+ .section {{
109
+ margin-bottom: 30px;
110
+ }}
111
+
112
+ .footer {{
113
+ text-align: center;
114
+ margin-top: 40px;
115
+ padding-top: 20px;
116
+ font-size: 12px;
117
+ color: #777;
118
+ border-top: 1px solid #eee;
119
+ }}
120
+
121
+ /* Custom styling for bullet points */
122
+ ul {{
123
+ list-style-type: disc;
124
+ }}
125
+ ul ul {{
126
+ list-style-type: circle;
127
+ }}
128
+ ul ul ul {{
129
+ list-style-type: square;
130
+ }}
131
+ </style>
132
+ </head>
133
+ <body>
134
+ <div class="report-header">
135
+ <div class="report-date">Ngày: {current_date}</div>
136
+ <h1 class="report-title">Bản tin Thị trường</h1>
137
+ <h2 class="report-subtitle">AI Financial Dashboard</h2>
138
+ </div>
139
+
140
+ <div class="report-body">
141
+ {html_content}
142
+ </div>
143
+
144
+ <div class="footer">
145
+ Bản tin được tạo tự động bởi AI Financial Dashboard. Thông tin chỉ mang tính chất tham khảo.
146
+ </div>
147
+ </body>
148
+ </html>
149
+ """
150
+
151
+ return full_html
152
+
153
+ def _generate_pdf_from_markdown(markdown_string):
154
+ """Tạo PDF từ Markdown sử dụng WeasyPrint"""
155
+ # Chuyển đổi markdown sang HTML
156
+ html_content = _markdown_to_html(markdown_string)
157
+
158
+ # Tạo file HTML tạm thời
159
+ with tempfile.NamedTemporaryFile(suffix='.html', delete=False) as temp_html:
160
+ temp_html_path = temp_html.name
161
+ temp_html.write(html_content.encode('utf-8'))
162
+
163
+ # Tạo file PDF từ HTML
164
+ try:
165
+ # Tạo tên file PDF tạm thời
166
+ with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as temp_pdf:
167
+ temp_pdf_path = temp_pdf.name
168
+
169
+ # Tạo PDF
170
+ weasyprint.HTML(filename=temp_html_path).write_pdf(temp_pdf_path)
171
+
172
+ # Đọc nội dung PDF
173
+ with open(temp_pdf_path, 'rb') as f:
174
+ pdf_data = f.read()
175
+
176
+ # Xóa các file tạm
177
+ os.unlink(temp_html_path)
178
+ os.unlink(temp_pdf_path)
179
+
180
+ return pdf_data
181
+ except Exception as e:
182
+ # Xử lý lỗi và đảm bảo xóa file tạm
183
+ if os.path.exists(temp_html_path):
184
+ os.unlink(temp_html_path)
185
+ raise e
186
+
187
+ def send_report_via_email(report_markdown, recipient_email):
188
+ """Gửi báo cáo thị trường qua email"""
189
+ try:
190
+ # Tạo PDF từ markdown
191
+ pdf_data = _generate_pdf_from_markdown(report_markdown)
192
+
193
+ # Tạo message
194
+ message = MIMEMultipart()
195
+ message["From"] = SENDER_EMAIL
196
+ message["To"] = recipient_email
197
+ message["Subject"] = f"AI Financial Dashboard - Bản tin Thị trường {datetime.now().strftime('%d/%m/%Y')}"
198
+
199
+ # Thêm phần nội dung với encoding UTF-8
200
+ body = """
201
+ Kính gửi Quý khách,
202
+
203
+ Đính kèm là bản tin thị trường tài chính hôm nay, được tổng hợp tự động bởi AI Financial Dashboard.
204
+
205
+ Trân trọng,
206
+ AI Financial Dashboard Team
207
+ """
208
+ message.attach(MIMEText(body, "plain", "utf-8"))
209
+
210
+ # Đính kèm file PDF
211
+ attachment = MIMEApplication(pdf_data, _subtype="pdf")
212
+ attachment.add_header(
213
+ "Content-Disposition", "attachment",
214
+ filename=f"Market_Report_{datetime.now().strftime('%Y%m%d')}.pdf"
215
+ )
216
+ message.attach(attachment)
217
+
218
+ # Kết nối đến server SMTP và gửi email
219
+ with smtplib.SMTP_SSL("smtp.gmail.com", 465) as server:
220
+ server.login(SENDER_EMAIL, SENDER_APP_PASSWORD)
221
+ server.send_message(message)
222
+
223
+ return True, "Email đã được gửi thành công!"
224
+
225
+ except Exception as e:
226
+ return False, f"Lỗi khi gửi email: {str(e)}"
227
+
228
+ if __name__ == "__main__":
229
+ report_markdown = """
230
+ # Bản tin Thị trường
231
+ ## Ngày 29/07/2025
232
+ - Cổ phiếu A tăng 10%
233
+ - Cổ phiếu B giảm 5%
234
+ """
235
+ send_report_via_email(report_markdown, "[email protected]")