anu151105 commited on
Commit
5cd1313
·
verified ·
1 Parent(s): ae12b71

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +205 -442
app.py CHANGED
@@ -1,481 +1,244 @@
1
- from langchain_groq import ChatGroq
2
- from langgraph.graph import StateGraph, START, END
3
- from IPython.display import Image, display, Markdown
4
- from typing_extensions import TypedDict
5
- from langgraph.constants import Send
6
- from langchain_core.messages import HumanMessage, SystemMessage
7
- from langchain_community.tools.tavily_search import TavilySearchResults
8
  import os
9
- import getpass
10
- from typing import Annotated, List, Dict, Any
11
- import operator
12
- from pydantic import BaseModel, Field
13
  from datetime import datetime
 
 
 
 
 
 
 
14
  import requests
15
  from bs4 import BeautifulSoup
16
- import re
17
- import json
18
- import gradio as gr
19
- from langdetect import detect
20
- from transformers import pipeline # Import Hugging Face pipeline for text generation
21
-
22
- # Define models for structured output
23
- class NewsItem(BaseModel):
24
- title: str = Field(description="Title of the news article")
25
- url: str = Field(description="URL of the news article")
26
- source: str = Field(description="Source website of the news")
27
- description: str = Field(description="Brief description of the news article")
28
-
29
- class NewsResults(BaseModel):
30
- news_items: List[NewsItem] = Field(description="List of news articles found")
31
 
32
- class Subsection(BaseModel):
33
- title: str = Field(description="Title of the subsection (based on news item title)")
34
- source: str = Field(description="Source of the news item")
35
- url: str = Field(description="URL of the news item")
36
- content: str = Field(description="Content for this subsection")
37
-
38
- class Section(BaseModel):
39
- name: str = Field(description="Name for this section of the blog")
40
- description: str = Field(description="Description for this section of the blog")
41
- information: str = Field(description="Information which should be included in this section of the blog")
42
- subsections: List[Subsection] = Field(description="Subsections for each news item in this category", default=[])
43
-
44
- class Sections(BaseModel):
45
- sections: List[Section] = Field(description="List of sections for this blog")
46
-
47
- # State definitions
48
  class NewsState(TypedDict):
49
- query: str
50
  date: str
51
  search_results: List[Dict[str, Any]]
52
  news_items: List[Dict[str, Any]]
53
-
54
- class BlogState(TypedDict):
55
- content: str
56
- sections: List[Section]
57
- completed_sections: Annotated[List, operator.add]
58
- final_report: str
59
-
60
- class WorkerState(TypedDict):
61
- section: Section
62
- completed_sections: Annotated[List, operator.add]
63
-
64
- class ArticleScraperState(TypedDict):
65
- url: str
66
- article_content: str
67
 
68
- # Helper function to detect English language
69
- def is_english(text):
70
- if not text or len(text.strip()) < 50:
71
- return False
72
-
 
 
 
73
  try:
74
- return detect(text) == 'en'
75
- except:
 
76
  return False
77
 
78
- # News search functions
79
- def search_general_news(state: NewsState):
80
- """Search for the latest news using Tavily based on the provided query."""
81
- search_tool = TavilySearchResults(max_results=10)
82
-
83
- today = state.get("date", datetime.now().strftime("%Y-%m-%d"))
84
- query = state.get("query", f"latest news {today} english")
85
 
 
 
 
 
 
 
 
86
  search_results = search_tool.invoke({"query": query})
87
-
88
- filtered_results = []
89
- for result in search_results:
90
- if "youtube.com" not in result.get("url", "").lower():
91
- content = result.get("content", "") + " " + result.get("title", "")
92
- if is_english(content):
93
- filtered_results.append(result)
94
-
95
- return {"search_results": filtered_results}
96
-
97
- def parse_news_items(state: NewsState):
98
- """Parse search results into structured news items using a more robust approach"""
99
- search_results = state["search_results"]
100
-
101
- formatted_results = "\n\n".join([
102
- f"Title: {result.get('title', 'No title')}\n"
103
- f"URL: {result.get('url', 'No URL')}\n"
104
- f"Content: {result.get('content', 'No content')}"
105
- for result in search_results
106
- ])
107
-
108
- system_prompt = """
109
- Extract news articles from these search results. Filter out any that aren't relevant.
110
-
111
- For each relevant news article, provide:
112
- - title: The title of the article
113
- - url: The URL of the article
114
- - source: The source website of the news
115
- - description: A brief description of the article
116
-
117
- Format your response as a JSON list of objects. Only include the relevant fields, nothing else.
118
  """
119
-
120
- response = llm.invoke([
121
- SystemMessage(content=system_prompt),
122
- HumanMessage(content=f"Here are the search results:\n\n{formatted_results}")
123
- ])
124
-
125
- response_text = response.content
126
- json_match = re.search(r'\[\s*\{.*\}\s*\]', response_text, re.DOTALL)
127
-
128
- news_items = []
129
- if json_match:
130
- try:
131
- news_items = json.loads(json_match.group(0))
132
- except json.JSONDecodeError:
133
- news_items = []
134
-
135
- return {"news_items": news_items}
136
-
137
- def scrape_article_content(state: ArticleScraperState):
138
- """Scrape the content from a news article URL"""
139
- url = state["url"]
140
-
141
  try:
142
- headers = {
143
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
144
- }
145
  response = requests.get(url, headers=headers, timeout=10)
146
  response.raise_for_status()
147
-
148
  soup = BeautifulSoup(response.text, 'html.parser')
149
-
150
- article_text = ""
151
- article = soup.find('article')
152
- if article:
153
- paragraphs = article.find_all('p')
154
- else:
155
- paragraphs = soup.find_all('p')
156
-
157
- article_text = "\n\n".join([p.get_text().strip() for p in paragraphs])
158
- article_text = re.sub(r'\s+', ' ', article_text).strip()
159
-
160
- if len(article_text) > 10000:
161
- article_text = article_text[:10000] + "..."
162
-
163
- if not is_english(article_text[:500]):
164
- return {"article_content": "Content not in English or insufficient text to analyze."}
165
-
166
- return {"article_content": article_text}
167
-
168
  except Exception as e:
169
- return {"article_content": f"Error scraping article: {str(e)}"}
 
170
 
171
- # Blog generation functions
172
- def orchestrator(state: BlogState):
173
- """Orchestrator that generates a plan for the blog based on news items"""
174
-
175
- try:
176
- content_lines = state['content'].split('\n\n')
177
- news_items = []
178
- current_item = {}
179
-
180
- for content_block in content_lines:
181
- if content_block.startswith('TITLE:'):
182
- if current_item and 'title' in current_item:
183
- news_items.append(current_item)
184
- current_item = {}
185
-
186
- lines = content_block.split('\n')
187
- for line in lines:
188
- if line.startswith('TITLE:'):
189
- current_item['title'] = line.replace('TITLE:', '').strip()
190
- elif line.startswith('SOURCE:'):
191
- current_item['source'] = line.replace('SOURCE:', '').strip()
192
- elif line.startswith('URL:'):
193
- current_item['url'] = line.replace('URL:', '').strip()
194
- elif line.startswith('DESCRIPTION:'):
195
- current_item['description'] = line.replace('DESCRIPTION:', '').strip()
196
- elif line.startswith('CONTENT:'):
197
- current_item['content'] = line.replace('CONTENT:', '').strip()
198
- elif 'content' in current_item:
199
- current_item['content'] += ' ' + content_block
200
-
201
- if current_item and 'title' in current_item:
202
- news_items.append(current_item)
203
-
204
- sections = []
205
-
206
- # Create sections with subsections
207
- for item in news_items:
208
- title = item.get('title', '').lower()
209
- description = item.get('description', '').lower()
210
- section_name = "General News"
211
- section_description = "Latest updates on various topics."
212
-
213
- subsections = [
214
- Subsection(
215
- title=item['title'],
216
- source=item['source'],
217
- url=item['url'],
218
- content=f"{item.get('description', '')} {item.get('content', '')[:500]}..."
219
- )
220
- ]
221
-
222
- sections.append(Section(
223
- name=section_name,
224
- description=section_description,
225
- information="Cover the latest developments in various topics.",
226
- subsections=subsections
227
- ))
228
-
229
- return {"sections": sections}
230
- except Exception as e:
231
- print(f"Error in orchestrator: {str(e)}")
232
- return {"sections": []}
233
-
234
- def llm_call(state: WorkerState):
235
- """Worker writes a section of the blog with subsections for each news item"""
236
-
237
- section = state['section']
238
-
239
- section_header = f"## {section.name}\n\n{section.description}\n"
240
-
241
- subsections_content = ""
242
- if section.subsections:
243
- for idx, subsection in enumerate(section.subsections):
244
- subsection_prompt = f"""
245
- Write a detailed subsection about this news item:
246
- Title: {subsection.title}
247
- Source: {subsection.source}
248
- URL: {subsection.url}
249
- Content to summarize and expand on:
250
- {subsection.content}
251
- Keep your response focused on the news item and make it engaging. Use markdown formatting.
252
- """
253
-
254
- subsection_content = llm.invoke([
255
- SystemMessage(content="You are writing a subsection for a news blog. Write in a professional but engaging style. Include key details and insights. Use markdown formatting."),
256
- HumanMessage(content=subsection_prompt)
257
- ])
258
-
259
- formatted_subsection = f"### {subsection.title}\n\n"
260
- formatted_subsection += f"*Source: [{subsection.source}]({subsection.url})*\n\n"
261
- formatted_subsection += subsection_content.content
262
-
263
- subsections_content += formatted_subsection + "\n\n"
264
- else:
265
- section_content = llm.invoke([
266
- SystemMessage(content="Write a blog section following the provided name, description, and information. Include no preamble. Use markdown formatting."),
267
- HumanMessage(content=f"Here is the section name: {section.name}\nDescription: {section.description}\nInformation: {section.information}")
268
- ])
269
- subsections_content = section_content.content
270
-
271
- complete_section = section_header + subsections_content
272
-
273
- return {"completed_sections": [complete_section]}
274
-
275
- def synthesizer(state: BlogState):
276
- """Synthesize full blog from sections with proper formatting and hierarchical TOC"""
277
-
278
- completed_sections = state["completed_sections"]
279
-
280
- completed_report = "\n\n".join(completed_sections)
281
-
282
- today = datetime.now().strftime("%Y-%m-%d")
283
- blog_title = f"# News Roundup - {today}"
284
-
285
- intro = llm.invoke([
286
- SystemMessage(content="Write a brief introduction for a news roundup blog post. Keep it under 100 words. Be engaging and professional."),
287
- HumanMessage(content=f"Today's date is {today}. Write a brief introduction for a news roundup.")
288
- ])
289
-
290
- table_of_contents = "## Table of Contents\n\n"
291
-
292
- section_matches = re.findall(r'## ([^\n]+)', completed_report)
293
-
294
- for i, section_name in enumerate(section_matches, 1):
295
- section_anchor = section_name.lower().replace(' ', '-')
296
- table_of_contents += f"{i}. [{section_name}](#{section_anchor})\n"
297
-
298
- section_start = completed_report.find(f"## {section_name}")
299
- next_section_match = re.search(r'## ', completed_report[section_start+1:])
300
- if next_section_match:
301
- section_end = section_start + 1 + next_section_match.start()
302
- section_text = completed_report[section_start:section_end]
303
- else:
304
- section_text = completed_report[section_start:]
305
-
306
- subsection_matches = re.findall(r'### ([^\n]+)', section_text)
307
-
308
- for j, subsection_name in enumerate(subsection_matches, 1):
309
- subsection_anchor = subsection_name.lower().replace(' ', '-').replace(':', '').replace('?', '').replace('!', '').replace('.', '')
310
- table_of_contents += f" {i}.{j}. [{subsection_name}](#{subsection_anchor})\n"
311
-
312
- final_report = f"{blog_title}\n\n{intro.content}\n\n{table_of_contents}\n\n---\n\n{completed_report}\n\n---\n\n*This news roundup was automatically generated on {today}.*"
313
-
314
- return {"final_report": final_report}
315
-
316
- # Edge function to create workers for each section
317
- def assign_workers(state: BlogState):
318
- """Assign a worker to each section in the plan"""
319
-
320
- return [Send("llm_call", {"section": s}) for s in state["sections"]]
321
-
322
- # Main workflow functions
323
- def create_news_search_workflow():
324
- """Create a workflow for searching and parsing general news"""
325
- workflow = StateGraph(NewsState)
326
-
327
- workflow.add_node("search_general_news", search_general_news)
328
- workflow.add_node("parse_news_items", parse_news_items)
329
-
330
- workflow.add_edge(START, "search_general_news")
331
- workflow.add_edge("search_general_news", "parse_news_items")
332
- workflow.add_edge("parse_news_items", END)
333
-
334
- return workflow.compile()
335
-
336
- def create_article_scraper_workflow():
337
- """Create a workflow for scraping article content"""
338
- workflow = StateGraph(ArticleScraperState)
339
-
340
- workflow.add_node("scrape_article", scrape_article_content)
341
-
342
- workflow.add_edge(START, "scrape_article")
343
- workflow.add_edge("scrape_article", END)
344
-
345
- return workflow.compile()
346
-
347
- def create_blog_generator_workflow():
348
- """Create a workflow for generating the blog"""
349
- workflow = StateGraph(BlogState)
350
-
351
- workflow.add_node("orchestrator", orchestrator)
352
- workflow.add_node("llm_call", llm_call)
353
- workflow.add_node("synthesizer", synthesizer)
354
-
355
- workflow.add_edge(START, "orchestrator")
356
- workflow.add_conditional_edges("orchestrator", assign_workers, ["llm_call"])
357
- workflow.add_edge("llm_call", "synthesizer")
358
- workflow.add_edge("synthesizer", END)
359
-
360
- return workflow.compile()
361
-
362
- def generate_general_news_blog(groq_api_key=None, tavily_api_key=None, date=None, topic=None):
363
- """Main function to generate news blog based on a specified topic."""
364
- if groq_api_key:
365
- os.environ["GROQ_API_KEY"] = groq_api_key
366
- if tavily_api_key:
367
- os.environ["TAVILY_API_KEY"] = tavily_api_key
368
-
369
- global llm
370
- llm = ChatGroq(model="qwen-2.5-32b")
371
-
372
- if not date:
373
- today = datetime.now().strftime("%Y-%m-%d")
374
- else:
375
- today = date
376
-
377
- query = f"latest {topic} news {today} english" if topic else f"latest news {today} english"
378
-
379
- news_search = create_news_search_workflow()
380
- news_results = news_search.invoke({"query": query, "date": today})
381
-
382
- print(f"Found {len(news_results['news_items'])} news items")
383
-
384
- article_scraper = create_article_scraper_workflow()
385
- news_contents = []
386
-
387
- for item in news_results["news_items"]:
388
- print(f"Scraping: {item['title']} from {item['source']}")
389
- result = article_scraper.invoke({"url": item['url']})
390
-
391
- if "not in English" in result["article_content"]:
392
- print(f"Skipping non-English content: {item['title']}")
393
- continue
394
-
395
- news_contents.append({
396
- "title": item['title'],
397
- "url": item['url'],
398
- "source": item['source'],
399
- "description": item['description'],
400
- "content": result["article_content"]
401
- })
402
 
 
 
 
 
 
403
  if not news_contents:
404
- return "No English language news items found for the specified date. Please try a different date."
405
-
406
- formatted_content = "\n\n".join([f"TITLE: {item['title']}\nSOURCE: {item['source']}\nURL: {item['url']}\nDESCRIPTION: {item['description']}\nCONTENT: {item['content'][:2000]}..."
407
- for item in news_contents
408
- ])
409
-
410
- blog_generator = create_blog_generator_workflow()
411
- blog_result = blog_generator.invoke({
412
- "content": formatted_content,
413
- "completed_sections": []
414
- })
415
-
416
- return blog_result["final_report"]
417
-
418
- # Gradio UI
419
- def create_gradio_interface():
420
- """Create a Gradio interface for the General News Blog Generator"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
421
 
422
- def run_generation(groq_key, tavily_key, selected_date, topic):
423
- if not groq_key or not tavily_key:
424
- return "Please provide both API keys."
425
-
426
- try:
427
- result = generate_general_news_blog(groq_key, tavily_key, selected_date, topic)
428
- return result
429
- except Exception as e:
430
- return f"Error generating blog: {str(e)}"
431
 
432
- with gr.Blocks(title="General News Blog Generator") as demo:
433
- gr.Markdown("# General News Blog Generator")
434
- gr.Markdown("Generate a daily roundup of news articles on various topics.")
435
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
436
  with gr.Row():
437
  with gr.Column():
438
- groq_key = gr.Textbox(label="Groq API Key", placeholder="Enter your Groq API key", type="password")
439
- tavily_key = gr.Textbox(label="Tavily API Key", placeholder="Enter your Tavily API key", type="password")
440
- date_picker = gr.Textbox(label="Date (YYYY-MM-DD)", placeholder="Leave empty for today's date",
441
- value=datetime.now().strftime("%Y-%m-%d"))
442
- topic_input = gr.Textbox(label="Topic", placeholder="Enter a topic for news generation")
443
- with gr.Row():
444
- generate_button = gr.Button("Generate News Blog", variant="primary")
445
- clear_button = gr.Button("Clear Output")
446
-
447
  with gr.Column():
448
- status_text = gr.Textbox(label="Status", placeholder="Ready to generate", interactive=False)
449
- output_md = gr.Markdown("Your news blog will appear here.")
450
-
451
- generate_button.click(
452
- fn=lambda: "Generating News Blog... This may take several minutes.",
 
 
 
453
  inputs=None,
454
- outputs=status_text,
455
- queue=False
456
  ).then(
457
  fn=run_generation,
458
  inputs=[groq_key, tavily_key, date_picker, topic_input],
459
- outputs=output_md
460
  ).then(
461
- fn=lambda: "Blog generation complete!",
462
  inputs=None,
463
- outputs=status_text
464
  )
465
-
466
- clear_button.click(
467
- fn=lambda: ("Ready to generate", ""),
468
- inputs=None,
469
- outputs=[status_text, output_md]
470
- )
471
-
472
  return demo
473
 
474
- # Run the entire pipeline
475
  if __name__ == "__main__":
476
- try:
477
- demo = create_gradio_interface()
478
- demo.launch()
479
-
480
- except Exception as e:
481
- print(f"Error running the pipeline: {str(e)}")
 
 
 
 
 
 
 
 
1
  import os
2
+ import re
 
 
 
3
  from datetime import datetime
4
+ from typing import List, Dict, Any
5
+ from collections import Counter
6
+ import matplotlib.pyplot as plt
7
+ from textblob import TextBlob
8
+ from wordcloud import WordCloud
9
+ import gradio as gr
10
+ from langchain_community.utilities.tavily_search import TavilySearchResults
11
  import requests
12
  from bs4 import BeautifulSoup
13
+ import time
14
+ from urllib.parse import urlparse
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
+ # Define a typed dictionary for state management
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  class NewsState(TypedDict):
18
+ topic: str
19
  date: str
20
  search_results: List[Dict[str, Any]]
21
  news_items: List[Dict[str, Any]]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
 
23
+ def is_english(text: str) -> bool:
24
+ """Check if the text is primarily in English by attempting ASCII encoding.
25
+
26
+ Args:
27
+ text (str): Text to check.
28
+ Returns:
29
+ bool: True if text is English, False otherwise.
30
+ """
31
  try:
32
+ text.encode('ascii')
33
+ return True
34
+ except UnicodeEncodeError:
35
  return False
36
 
37
+ def search_news(state: NewsState) -> Dict[str, List[Dict[str, Any]]]:
38
+ """Search for news articles using Tavily based on the topic and date.
 
 
 
 
 
39
 
40
+ Args:
41
+ state (NewsState): State containing topic and date.
42
+ Returns:
43
+ dict: Dictionary with search results limited to 5 articles.
44
+ """
45
+ search_tool = TavilySearchResults(max_results=10)
46
+ query = f"latest {state['topic']} news {state['date']} english"
47
  search_results = search_tool.invoke({"query": query})
48
+ # Filter out YouTube links and non-English content
49
+ filtered_results = [
50
+ result for result in search_results
51
+ if "youtube.com" not in result.get("url", "").lower()
52
+ and is_english(result.get("content", "") + " " + result.get("title", ""))
53
+ ]
54
+ return {"search_results": filtered_results[:5]} # Limit to 5 for performance
55
+
56
+ def scrape_article_content(url: str) -> str:
57
+ """Scrape article content from the URL with error handling and ethical practices.
58
+
59
+ Args:
60
+ url (str): URL of the article to scrape.
61
+ Returns:
62
+ str: Scraped content or error message.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  try:
65
+ headers = {'User-Agent': 'NewsBlogGenerator/1.0 (https://example.com/bot)'}
 
 
66
  response = requests.get(url, headers=headers, timeout=10)
67
  response.raise_for_status()
 
68
  soup = BeautifulSoup(response.text, 'html.parser')
69
+ paragraphs = soup.find_all('p')
70
+ article_text = " ".join([p.get_text().strip() for p in paragraphs])[:2000] # Limit content length
71
+ if not article_text or not is_english(article_text[:500]):
72
+ return "Content unavailable or not in English."
73
+ time.sleep(1) # Delay to respect server load
74
+ return article_text
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  except Exception as e:
76
+ print(f"Failed to scrape {url}: {e}")
77
+ return f"Scraping failed: {str(e)}"
78
 
79
+ def generate_source_count_figure(news_contents: List[Dict[str, str]]):
80
+ """Generate a bar chart of news sources if content is available.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
 
82
+ Args:
83
+ news_contents (List[Dict]): List of news items with source info.
84
+ Returns:
85
+ matplotlib.figure.Figure or None: Bar chart figure or None if no content.
86
+ """
87
  if not news_contents:
88
+ return None
89
+ sources = [item['source'] for item in news_contents]
90
+ source_count = Counter(sources)
91
+ fig, ax = plt.subplots(figsize=(8, 4))
92
+ source_count_sorted = dict(sorted(source_count.items(), key=lambda x: x[1], reverse=True))
93
+ ax.bar(source_count_sorted.keys(), source_count_sorted.values())
94
+ ax.set_title("News Source Distribution")
95
+ plt.xticks(rotation=45, ha='right')
96
+ plt.tight_layout()
97
+ return fig
98
+
99
+ def generate_sentiment_analysis_figure(news_contents: List[Dict[str, str]]):
100
+ """Generate a pie chart of sentiment distribution if content is available.
101
+
102
+ Args:
103
+ news_contents (List[Dict]): List of news items with content.
104
+ Returns:
105
+ matplotlib.figure.Figure or None: Pie chart figure or None if no content.
106
+ """
107
+ if not news_contents:
108
+ return None
109
+ sentiments = []
110
+ for item in news_contents:
111
+ text = item['content'][:1000] # Limit for performance
112
+ blob = TextBlob(text)
113
+ polarity = blob.sentiment.polarity
114
+ sentiment = "positive" if polarity > 0 else "negative" if polarity < 0 else "neutral"
115
+ sentiments.append(sentiment)
116
+ sentiment_count = Counter(sentiments)
117
+ fig, ax = plt.subplots()
118
+ ax.pie(sentiment_count.values(), labels=sentiment_count.keys(), autopct='%1.1f%%', startangle=90)
119
+ ax.set_title("Sentiment Analysis")
120
+ return fig
121
+
122
+ def generate_word_cloud_figure(news_contents: List[Dict[str, str]]):
123
+ """Generate a word cloud from news content if content is available.
124
+
125
+ Args:
126
+ news_contents (List[Dict]): List of news items with content.
127
+ Returns:
128
+ matplotlib.figure.Figure or None: Word cloud figure or None if no content.
129
+ """
130
+ if not news_contents:
131
+ return None
132
+ all_text = ' '.join([clean_text(item['content']) for item in news_contents])
133
+ stopwords = {"the", "and", "in", "to", "of", "is", "for", "with", "on", "that", "this", "are", "was", "be", "have", "it", "not", "they", "by", "from"}
134
+ wordcloud = WordCloud(width=800, height=400, background_color='white',
135
+ stopwords=stopwords, max_words=50, min_word_length=3).generate(all_text)
136
+ fig = plt.figure(figsize=(8, 4))
137
+ plt.imshow(wordcloud, interpolation='bilinear')
138
+ plt.axis("off")
139
+ return fig
140
+
141
+ def clean_text(text: str) -> str:
142
+ """Clean text by removing punctuation and converting to lowercase.
143
+
144
+ Args:
145
+ text (str): Text to clean.
146
+ Returns:
147
+ str: Cleaned text.
148
+ """
149
+ text = re.sub(r'[^\w\s]', '', text.lower())
150
+ return text
151
+
152
+ def generate_blog(news_contents: List[Dict[str, str]], topic: str, date: str) -> str:
153
+ """Generate a Markdown blog post from the news contents.
154
+
155
+ Args:
156
+ news_contents (List[Dict]): List of news items.
157
+ topic (str): Topic of the blog.
158
+ date (str): Date of the news.
159
+ Returns:
160
+ str: Markdown-formatted blog content.
161
+ """
162
+ if not news_contents:
163
+ return f"# No News Found\n\nSorry, no articles were found for '{topic}' on {date}."
164
+ blog = f"# Latest {topic.capitalize()} News - {date}\n\nHere’s a roundup of the latest in {topic}:\n\n"
165
+ for i, item in enumerate(news_contents, 1):
166
+ blog += f"### {i}. {item['title']}\n\n{item['content'][:500]}...\n\n[Read more]({item['url']})\n\n"
167
+ return blog
168
+
169
+ def run_generation(groq_key: str, tavily_key: str, date: str, topic: str):
170
+ """Run the entire generation process: search, scrape, generate blog and visualizations.
171
+
172
+ Args:
173
+ groq_key (str): Groq API key.
174
+ tavily_key (str): Tavily API key.
175
+ date (str): Date in YYYY-MM-DD format.
176
+ topic (str): Topic to search for.
177
+ Returns:
178
+ tuple: Blog content and three visualization figures.
179
+ """
180
+ # Set API keys in environment
181
+ os.environ["GROQ_API_KEY"] = groq_key
182
+ os.environ["TAVILY_API_KEY"] = tavily_key
183
 
184
+ # Initialize state
185
+ state = NewsState(topic=topic, date=date, search_results=[], news_items=[])
186
+ state.update(search_news(state))
 
 
 
 
 
 
187
 
188
+ # Scrape content and filter successful results
189
+ news_contents = []
190
+ for item in state["search_results"]:
191
+ content = scrape_article_content(item["url"])
192
+ if "failed" not in content and "unavailable" not in content:
193
+ source = urlparse(item["url"]).netloc
194
+ news_contents.append({
195
+ "title": item.get("title", "Untitled"),
196
+ "url": item["url"],
197
+ "source": source,
198
+ "content": content
199
+ })
200
+
201
+ # Generate outputs
202
+ blog_md = generate_blog(news_contents, topic, date)
203
+ source_fig = generate_source_count_figure(news_contents)
204
+ sentiment_fig = generate_sentiment_analysis_figure(news_contents)
205
+ wordcloud_fig = generate_word_cloud_figure(news_contents)
206
+
207
+ return blog_md, source_fig, sentiment_fig, wordcloud_fig
208
+
209
+ def create_interface():
210
+ """Create and return the Gradio interface."""
211
+ with gr.Blocks(title="News Blog Generator") as demo:
212
+ gr.Markdown("# News Blog Generator")
213
  with gr.Row():
214
  with gr.Column():
215
+ groq_key = gr.Textbox(label="Groq API Key", type="password")
216
+ tavily_key = gr.Textbox(label="Tavily API Key", type="password")
217
+ date_picker = gr.Textbox(label="Date (YYYY-MM-DD)", value=datetime.now().strftime("%Y-%m-%d"))
218
+ topic_input = gr.Textbox(label="Topic", placeholder="e.g., AI, Blockchain")
219
+ generate_btn = gr.Button("Generate")
 
 
 
 
220
  with gr.Column():
221
+ status = gr.Textbox(label="Status", value="Ready", interactive=False)
222
+ blog_output = gr.Markdown(label="Generated Blog")
223
+ source_plot = gr.Plot(label="Source Count")
224
+ sentiment_plot = gr.Plot(label="Sentiment Analysis")
225
+ wordcloud_plot = gr.Plot(label="Word Cloud")
226
+ # Chain events for progress feedback
227
+ generate_btn.click(
228
+ fn=lambda: "Searching and generating blog...",
229
  inputs=None,
230
+ outputs=status
 
231
  ).then(
232
  fn=run_generation,
233
  inputs=[groq_key, tavily_key, date_picker, topic_input],
234
+ outputs=[blog_output, source_plot, sentiment_plot, wordcloud_plot]
235
  ).then(
236
+ fn=lambda: "Generation complete!",
237
  inputs=None,
238
+ outputs=status
239
  )
 
 
 
 
 
 
 
240
  return demo
241
 
 
242
  if __name__ == "__main__":
243
+ interface = create_interface()
244
+ interface.launch()