lization commited on
Commit
88fe2e0
ยท
verified ยท
1 Parent(s): 2f3fcd6

Upload 3 files

Browse files
Files changed (3) hide show
  1. README (1).md +71 -0
  2. app (1).py +403 -0
  3. requirements (1).txt +7 -0
README (1).md ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Trend Longevity Analyser
3
+ emoji: ๐Ÿ“Š
4
+ colorFrom: blue
5
+ colorTo: indigo
6
+ sdk: gradio
7
+ sdk_version: 4.44.0
8
+ app_file: app.py
9
+ pinned: true
10
+ ---
11
+
12
+ # ๐Ÿ“Š Trend Longevity Analyser
13
+
14
+ A **RAG-powered trend intelligence tool** that classifies whether any topic is *Early, Rising, at Peak, Declining, or Fading* โ€” using live news data.
15
+
16
+ ## How It Works
17
+
18
+ This tool implements a full 6-step RAG (Retrieval-Augmented Generation) pipeline:
19
+
20
+ | Step | What Happens |
21
+ |------|-------------|
22
+ | **1. Fetch** | NewsAPI pulls up to 50 recent articles on your topic |
23
+ | **2. Chunk** | LangChain splits articles into overlapping 400-character chunks |
24
+ | **3. Embed** | `all-MiniLM-L6-v2` encodes each chunk into a 384-dim vector |
25
+ | **4. Store** | ChromaDB holds all vectors in an in-memory collection |
26
+ | **5. Retrieve** | Semantic search returns the 10 most relevant chunks |
27
+ | **6. Generate** | Claude analyses the retrieved context and returns a structured trend signal |
28
+
29
+ ## Tech Stack
30
+
31
+ - **Retrieval:** [ChromaDB](https://www.trychroma.com) (vector store) + [Sentence Transformers](https://sbert.net) (embeddings)
32
+ - **Orchestration:** [LangChain](https://langchain.com) text splitting
33
+ - **News Data:** [NewsAPI](https://newsapi.org) (free tier)
34
+ - **Generation:** [Google Gemini Flash](https://aistudio.google.com) (free tier)
35
+ - **UI:** [Gradio](https://gradio.app)
36
+
37
+ ## Setup
38
+
39
+ ### Running Locally
40
+
41
+ ```bash
42
+ git clone https://huggingface.co/spaces/lization/trend-longevity-analyser
43
+ cd trend-longevity-analyser
44
+ pip install -r requirements.txt
45
+ export NEWSAPI_KEY=your_key_here
46
+ export ANTHROPIC_API_KEY=your_key_here
47
+ python app.py
48
+ ```
49
+
50
+ ### HF Space Secrets
51
+
52
+ Set `NEWSAPI_KEY` and `GEMINI_API_KEY` as Space Secrets under **Settings โ†’ Variables and Secrets**. If not set, users can enter keys directly in the UI.
53
+
54
+ ## Get Your Free API Keys
55
+
56
+ - **NewsAPI:** [newsapi.org/register](https://newsapi.org/register) โ€” free tier, 100 requests/day, last 30 days of articles
57
+ - **Google Gemini:** [aistudio.google.com](https://aistudio.google.com) โ€” free tier, 1,500 requests/day, no credit card needed
58
+
59
+ ## Why RAG (Not Just a Prompt)?
60
+
61
+ Instead of passing all 50 articles to Claude (expensive, noisy, hits context limits), the RAG approach:
62
+
63
+ 1. Embeds every chunk as a vector
64
+ 2. Retrieves only the **10 most semantically relevant chunks** via cosine similarity
65
+ 3. Passes those to Claude for analysis
66
+
67
+ This gives more focused, accurate, and cost-efficient results โ€” and is directly analogous to production social listening systems used in enterprise contexts.
68
+
69
+ ---
70
+
71
+ Built by [Sammie Wong](https://linkedin.com/in/)
app (1).py ADDED
@@ -0,0 +1,403 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Trend Longevity Analyser
3
+ ========================
4
+ A RAG-powered tool that classifies whether a topic/trend is Early, Rising,
5
+ at Peak, Declining, or Fading โ€” based on live news data.
6
+
7
+ RAG Pipeline (6 steps):
8
+ 1. FETCH โ†’ NewsAPI pulls up to 50 recent articles on the topic
9
+ 2. CHUNK โ†’ LangChain splits each article into overlapping 400-char chunks
10
+ 3. EMBED โ†’ Sentence Transformers encodes every chunk as a dense vector
11
+ 4. STORE โ†’ ChromaDB holds all vectors in an in-memory collection
12
+ 5. RETRIEVE โ†’ Semantic search returns the 10 most relevant chunks
13
+ 6. GENERATE โ†’ Gemini Flash analyses the retrieved context and returns a structured signal
14
+
15
+ Built by: Sammie Wong
16
+ """
17
+
18
+ import os
19
+ import json
20
+ import uuid
21
+ import gradio as gr
22
+ import google.generativeai as genai
23
+ import chromadb
24
+ from chromadb.utils import embedding_functions
25
+ from newsapi import NewsApiClient
26
+ from langchain.text_splitter import RecursiveCharacterTextSplitter
27
+ from datetime import datetime, timedelta
28
+
29
+
30
+ # โ”€โ”€โ”€ STEP 1: FETCH โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
31
+
32
+ def fetch_articles(topic: str, days_back: int, newsapi_key: str) -> list[dict]:
33
+ """
34
+ Pull recent news articles from NewsAPI for the given topic.
35
+ Free tier: up to 100 requests/day, articles from the last 30 days.
36
+ """
37
+ client = NewsApiClient(api_key=newsapi_key)
38
+ from_date = (datetime.now() - timedelta(days=days_back)).strftime('%Y-%m-%d')
39
+
40
+ response = client.get_everything(
41
+ q=f'"{topic}"', # Exact phrase match for cleaner signal
42
+ language='en',
43
+ sort_by='publishedAt',
44
+ from_param=from_date,
45
+ page_size=50 # NewsAPI free tier maximum
46
+ )
47
+ return response.get('articles', [])
48
+
49
+
50
+ # โ”€โ”€โ”€ STEP 2: CHUNK โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
51
+
52
+ def chunk_articles(articles: list[dict]) -> tuple[list[str], list[dict]]:
53
+ """
54
+ Split each article into overlapping chunks.
55
+
56
+ Why chunk? LLMs have context limits and RAG works better with focused
57
+ pieces of text than entire articles. Overlap (40 chars) prevents
58
+ losing meaning at chunk boundaries.
59
+ """
60
+ splitter = RecursiveCharacterTextSplitter(
61
+ chunk_size=400, # ~80 words โ€” enough context per chunk
62
+ chunk_overlap=40 # Overlap prevents boundary information loss
63
+ )
64
+
65
+ all_chunks, all_metadata = [], []
66
+
67
+ for article in articles:
68
+ # Combine title + description + content for the richest signal
69
+ full_text = " ".join(filter(None, [
70
+ article.get('title', ''),
71
+ article.get('description', ''),
72
+ article.get('content', '')
73
+ ]))
74
+
75
+ for chunk in splitter.split_text(full_text):
76
+ all_chunks.append(chunk)
77
+ all_metadata.append({
78
+ 'source': article['source']['name'],
79
+ 'published_at': article['publishedAt'][:10],
80
+ 'title': (article.get('title') or '')[:100]
81
+ })
82
+
83
+ return all_chunks, all_metadata
84
+
85
+
86
+ # โ”€โ”€โ”€ STEPS 3 & 4: EMBED + STORE โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
87
+
88
+ def build_vector_store(chunks: list[str], metadatas: list[dict]):
89
+ """
90
+ Encode every chunk into a 384-dimensional embedding vector and store
91
+ in an in-memory ChromaDB collection.
92
+
93
+ Model: all-MiniLM-L6-v2 โ€” fast, accurate, no GPU needed.
94
+ Storage: EphemeralClient (in-memory) means no disk writes โ€” perfect for
95
+ a serverless HF Space where each request is stateless.
96
+ """
97
+ embedding_fn = embedding_functions.SentenceTransformerEmbeddingFunction(
98
+ model_name="all-MiniLM-L6-v2"
99
+ )
100
+
101
+ try:
102
+ db = chromadb.EphemeralClient() # ChromaDB >= 0.4.x
103
+ except AttributeError:
104
+ db = chromadb.Client() # Fallback for older versions
105
+
106
+ collection = db.create_collection(
107
+ name=f"trend_{uuid.uuid4().hex[:8]}",
108
+ embedding_function=embedding_fn
109
+ )
110
+
111
+ # Insert in batches of 100 to avoid memory spikes
112
+ batch_size = 100
113
+ for i in range(0, len(chunks), batch_size):
114
+ batch_end = i + batch_size
115
+ collection.add(
116
+ documents=chunks[i:batch_end],
117
+ metadatas=metadatas[i:batch_end],
118
+ ids=[f"c{i + j}" for j in range(len(chunks[i:batch_end]))]
119
+ )
120
+
121
+ return collection
122
+
123
+
124
+ # โ”€โ”€โ”€ STEP 5: RETRIEVE โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
125
+
126
+ def retrieve_context(collection, topic: str, n_results: int = 10) -> tuple[list[str], list[dict]]:
127
+ """
128
+ Run a semantic similarity search to retrieve the most relevant chunks.
129
+
130
+ The query is phrased to surface trend-signal language โ€” not just
131
+ topic mentions. ChromaDB uses cosine similarity on the embeddings.
132
+ """
133
+ query = f"trend momentum growth decline viral popularity media coverage {topic}"
134
+
135
+ results = collection.query(
136
+ query_texts=[query],
137
+ n_results=min(n_results, collection.count())
138
+ )
139
+ return results['documents'][0], results['metadatas'][0]
140
+
141
+
142
+ # โ”€โ”€โ”€ STEP 6: GENERATE โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
143
+
144
+ def analyse_with_gemini(
145
+ topic: str,
146
+ retrieved_docs: list[str],
147
+ retrieved_meta: list[dict],
148
+ total_articles: int,
149
+ days_back: int,
150
+ gemini_key: str
151
+ ) -> dict:
152
+ """
153
+ Pass the retrieved chunks (NOT all articles) to Gemini Flash for analysis.
154
+
155
+ This is the core RAG advantage: the model only sees the most semantically
156
+ relevant evidence, which produces more focused and accurate analysis
157
+ than dumping all 50 articles into the prompt.
158
+
159
+ response_mime_type="application/json" tells Gemini to return clean JSON
160
+ directly โ€” no markdown fences, no preamble, no parsing headaches.
161
+ """
162
+ context = "\n\n".join([
163
+ f"[{m['source']} ยท {m['published_at']}]\n{doc}"
164
+ for doc, m in zip(retrieved_docs, retrieved_meta)
165
+ ])
166
+
167
+ prompt = f"""You are a senior trend analyst at a social listening company (like Pulsar, Brandwatch, or Sprinklr).
168
+
169
+ You have been given the {len(retrieved_docs)} most semantically relevant news excerpts about "{topic}",
170
+ retrieved via RAG from a corpus of {total_articles} articles published in the last {days_back} days.
171
+
172
+ RETRIEVED CONTEXT:
173
+ {context}
174
+
175
+ Based on this evidence, classify the trend and return a JSON object with exactly these fields:
176
+ {{
177
+ "phase": "Early|Rising|Peak|Declining|Fading",
178
+ "confidence": <integer 0-100>,
179
+ "momentum_score": <integer 0-100>,
180
+ "summary": "<2-3 sentence narrative of the trend's current state and why>",
181
+ "key_signals": [
182
+ "<specific evidence signal from the articles>",
183
+ "<specific evidence signal from the articles>",
184
+ "<specific evidence signal from the articles>",
185
+ "<specific evidence signal from the articles>"
186
+ ],
187
+ "prediction_30d": "<one concrete sentence: what will likely happen to this trend in 30 days>",
188
+ "media_spread": "Niche|Specialist|Mainstream|Viral",
189
+ "top_themes": ["<theme 1>", "<theme 2>", "<theme 3>"]
190
+ }}
191
+
192
+ Phase definitions:
193
+ - Early: Emerging signal, limited coverage, mostly specialist/niche sources
194
+ - Rising: Volume growing, broadening coverage, sentiment intensifying
195
+ - Peak: Maximum velocity, mainstream saturation, brand/celebrity involvement
196
+ - Declining: Volume dropping, novelty fading, sentiment normalising
197
+ - Fading: Minimal coverage, topic becoming dated, resolved, or replaced"""
198
+
199
+ genai.configure(api_key=gemini_key)
200
+ model = genai.GenerativeModel(
201
+ model_name="gemini-2.0-flash",
202
+ generation_config=genai.GenerationConfig(
203
+ response_mime_type="application/json", # Forces clean JSON output
204
+ max_output_tokens=900,
205
+ )
206
+ )
207
+ response = model.generate_content(prompt)
208
+ return json.loads(response.text)
209
+
210
+
211
+ # โ”€โ”€โ”€ PIPELINE ORCHESTRATOR โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
212
+
213
+ def run_analysis(topic: str, days_back: int, newsapi_key_input: str, gemini_key_input: str):
214
+ """
215
+ Runs the full 6-step RAG pipeline and returns formatted Gradio outputs.
216
+ Returns 6 values matching the 6 gr.Markdown output components.
217
+ """
218
+ EMPTY = ("", "", "", "", "", "")
219
+
220
+ if not topic.strip():
221
+ return ("โš ๏ธ Please enter a topic.", *EMPTY[1:])
222
+
223
+ # Resolve API keys: UI input โ†’ environment variable โ†’ error
224
+ news_key = newsapi_key_input.strip() or os.environ.get("NEWSAPI_KEY", "")
225
+ gemini_key = gemini_key_input.strip() or os.environ.get("GEMINI_API_KEY", "")
226
+
227
+ if not news_key:
228
+ return ("โš ๏ธ NewsAPI key is required. Get a free key at newsapi.org", *EMPTY[1:])
229
+ if not gemini_key:
230
+ return ("โš ๏ธ Gemini API key is required. Get a free key at aistudio.google.com", *EMPTY[1:])
231
+
232
+ try:
233
+ # โ”€โ”€ Step 1: Fetch โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
234
+ articles = fetch_articles(topic, int(days_back), news_key)
235
+ if not articles:
236
+ return (
237
+ f"โš ๏ธ No articles found for **'{topic}'** in the last {days_back} days.\n\n"
238
+ "Try a broader topic, shorter name, or longer date range.",
239
+ *EMPTY[1:]
240
+ )
241
+
242
+ # โ”€โ”€ Steps 2โ€“4: Chunk โ†’ Embed โ†’ Store โ”€โ”€โ”€โ”€โ”€๏ฟฝ๏ฟฝโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
243
+ chunks, metadatas = chunk_articles(articles)
244
+ collection = build_vector_store(chunks, metadatas)
245
+
246
+ # โ”€โ”€ Step 5: Retrieve โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
247
+ retrieved_docs, retrieved_meta = retrieve_context(collection, topic)
248
+
249
+ # โ”€โ”€ Step 6: Generate โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
250
+ result = analyse_with_gemini(
251
+ topic, retrieved_docs, retrieved_meta,
252
+ len(articles), int(days_back), gemini_key
253
+ )
254
+
255
+ # โ”€โ”€ Format outputs โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
256
+ phase_icons = {
257
+ "Early": "๐ŸŒฑ", "Rising": "๐Ÿ“ˆ", "Peak": "๐Ÿ”ฅ",
258
+ "Declining": "๐Ÿ“‰", "Fading": "๐ŸŒซ๏ธ"
259
+ }
260
+ phase_colors = {
261
+ "Early": "#22c55e", "Rising": "#3b82f6", "Peak": "#f97316",
262
+ "Declining": "#ef4444", "Fading": "#94a3b8"
263
+ }
264
+ phase = result.get("phase", "Unknown")
265
+ icon = phase_icons.get(phase, "โ“")
266
+ color = phase_colors.get(phase, "#6b7280")
267
+
268
+ phase_md = f"## {icon} {phase.upper()}"
269
+
270
+ scores_md = f"""### ๐Ÿ“Š Metrics
271
+ | | |
272
+ |---|---|
273
+ | **Confidence** | `{result.get('confidence', 'โ€”')}%` |
274
+ | **Momentum Score** | `{result.get('momentum_score', 'โ€”')} / 100` |
275
+ | **Media Spread** | `{result.get('media_spread', 'โ€”')}` |
276
+ | **Articles Fetched** | `{len(articles)}` |
277
+ | **Chunks in Vector Store** | `{len(chunks)}` |
278
+ | **Chunks Retrieved via RAG** | `{len(retrieved_docs)}` |"""
279
+
280
+ summary_md = f"### ๐Ÿ“ Narrative\n{result.get('summary', '')}"
281
+
282
+ signals_md = "### ๐Ÿ” Key Signals\n" + "\n".join(
283
+ [f"- {s}" for s in result.get('key_signals', [])]
284
+ ) + "\n\n**Top Themes:** " + " ".join(
285
+ [f"`{t}`" for t in result.get('top_themes', [])]
286
+ )
287
+
288
+ prediction_md = f"### ๐Ÿ”ฎ 30-Day Prediction\n_{result.get('prediction_30d', '')}_"
289
+
290
+ sources_md = "### ๐Ÿ“ฐ Articles Used in RAG Retrieval\n" + "\n".join([
291
+ f"- **{m['source']}** ({m['published_at']}): _{m['title']}_"
292
+ for m in retrieved_meta[:6]
293
+ ])
294
+
295
+ return phase_md, scores_md, summary_md, signals_md, prediction_md, sources_md
296
+
297
+ except json.JSONDecodeError:
298
+ return ("โš ๏ธ Could not parse analysis response. Please try again.", *EMPTY[1:])
299
+ except Exception as e:
300
+ return (f"โš ๏ธ Error: {str(e)}", *EMPTY[1:])
301
+
302
+
303
+ # โ”€โ”€โ”€ GRADIO UI โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
304
+
305
+ CSS = """
306
+ #title-block { text-align: center; padding: 8px 0 4px 0; }
307
+ #title-block h1 { font-size: 2rem; font-weight: 800; margin-bottom: 2px; }
308
+ #title-block p { color: #64748b; font-size: 0.95rem; }
309
+ .phase-box { text-align: center; font-size: 1.8rem; font-weight: 800;
310
+ padding: 18px; background: #f8fafc; border-radius: 12px;
311
+ border: 2px solid #e2e8f0; min-height: 80px; }
312
+ .metric-card { background: #f8fafc; border-radius: 10px; padding: 4px 8px;
313
+ border-left: 3px solid #3b82f6; }
314
+ footer { display: none !important; }
315
+ """
316
+
317
+ HOWTO = """
318
+ **Step 1 โ€” Fetch:** NewsAPI retrieves up to 50 articles matching your topic from the selected date range.
319
+
320
+ **Step 2 โ€” Chunk:** LangChain's `RecursiveCharacterTextSplitter` breaks each article into overlapping 400-character chunks (~80 words). Overlap prevents information loss at chunk boundaries.
321
+
322
+ **Step 3 โ€” Embed:** Each chunk is encoded into a 384-dimensional dense vector using `all-MiniLM-L6-v2` (Sentence Transformers) โ€” optimised for semantic similarity, runs without a GPU.
323
+
324
+ **Step 4 โ€” Store:** All vectors are loaded into a ChromaDB `EphemeralClient` collection (fully in-memory โ€” no disk writes, stateless per request).
325
+
326
+ **Step 5 โ€” Retrieve:** A trend-signal query is run against the vector store. ChromaDB uses cosine similarity to return the 10 most semantically relevant chunks from across all articles.
327
+
328
+ **Step 6 โ€” Generate:** Gemini Flash receives *only* the retrieved chunks as context โ€” not all 50 articles. This is the core RAG advantage: focused, grounded, efficient analysis with a minimal prompt. `response_mime_type="application/json"` guarantees clean structured output.
329
+ """
330
+
331
+ with gr.Blocks(theme=gr.themes.Soft(
332
+ primary_hue="blue",
333
+ secondary_hue="slate",
334
+ font=[gr.themes.GoogleFont("DM Sans"), "ui-sans-serif", "sans-serif"]
335
+ ), css=CSS, title="Trend Longevity Analyser") as demo:
336
+
337
+ with gr.Column(elem_id="title-block"):
338
+ gr.Markdown("""
339
+ # ๐Ÿ“Š Trend Longevity Analyser
340
+ **RAG-powered trend intelligence** ยท NewsAPI + ChromaDB + Sentence Transformers + Gemini Flash
341
+ """)
342
+
343
+ gr.Markdown("---")
344
+
345
+ # โ”€โ”€ Inputs โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
346
+ with gr.Accordion("๐Ÿ”‘ API Keys", open=False):
347
+ gr.Markdown(
348
+ "_Keys entered here are used only for your request and never stored. "
349
+ "If you're hosting this Space privately, set `NEWSAPI_KEY` and "
350
+ "`GEMINI_API_KEY` as Secrets instead._"
351
+ )
352
+ with gr.Row():
353
+ newsapi_input = gr.Textbox(label="NewsAPI Key", placeholder="Get free key at newsapi.org", type="password")
354
+ gemini_input = gr.Textbox(label="Google Gemini API Key", placeholder="Get free key at aistudio.google.com", type="password")
355
+
356
+ with gr.Row():
357
+ with gr.Column(scale=4):
358
+ topic_input = gr.Textbox(
359
+ label="Topic / Trend",
360
+ placeholder="e.g. 'Gen Z workplace burnout' ยท 'green hydrogen' ยท 'AI companions'",
361
+ lines=1
362
+ )
363
+ with gr.Column(scale=1):
364
+ days_input = gr.Slider(label="Days Back", minimum=7, maximum=30, value=30, step=7,
365
+ info="Free NewsAPI tier = 30 days max")
366
+
367
+ analyse_btn = gr.Button("๐Ÿ” Analyse Trend", variant="primary", size="lg")
368
+
369
+ gr.Markdown("---")
370
+
371
+ # โ”€โ”€ Outputs โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
372
+ phase_output = gr.Markdown(elem_classes=["phase-box"])
373
+
374
+ with gr.Row():
375
+ with gr.Column():
376
+ scores_output = gr.Markdown(elem_classes=["metric-card"])
377
+ signals_output = gr.Markdown()
378
+ with gr.Column():
379
+ summary_output = gr.Markdown()
380
+ prediction_output = gr.Markdown()
381
+
382
+ sources_output = gr.Markdown()
383
+
384
+ with gr.Accordion("๐Ÿง  How the RAG Pipeline Works", open=False):
385
+ gr.Markdown(HOWTO)
386
+
387
+ gr.Markdown(
388
+ "_Built by [Sammie Wong](https://linkedin.com/in/) ยท "
389
+ "Source: [GitHub](https://github.com/) ยท "
390
+ "Powered by [NewsAPI](https://newsapi.org) + [ChromaDB](https://www.trychroma.com) + "
391
+ "[Gemini Flash](https://aistudio.google.com)_",
392
+ )
393
+
394
+ # โ”€โ”€ Wire up โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
395
+ analyse_btn.click(
396
+ fn=run_analysis,
397
+ inputs=[topic_input, days_input, newsapi_input, gemini_input],
398
+ outputs=[phase_output, scores_output, summary_output,
399
+ signals_output, prediction_output, sources_output]
400
+ )
401
+
402
+ if __name__ == "__main__":
403
+ demo.launch()
requirements (1).txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ gradio>=4.40.0
2
+ google-generativeai>=0.8.0
3
+ newsapi-python>=0.2.7
4
+ langchain>=0.2.0
5
+ langchain-text-splitters>=0.2.0
6
+ chromadb>=0.5.0
7
+ sentence-transformers>=3.0.0