Spaces:
Running
Running
Final Update
Browse files- app/api/__pycache__/main.cpython-312.pyc +0 -0
- app/api/main.py +73 -24
- app/services/__pycache__/news_client.cpython-312.pyc +0 -0
- app/services/news_client.py +55 -0
- reputation_logs.csv +62 -0
- requirements.txt +6 -9
- streamlit_app/app.py +84 -0
- test_scraper.py +8 -0
- tests/test_api.py +18 -0
app/api/__pycache__/main.cpython-312.pyc
CHANGED
|
Binary files a/app/api/__pycache__/main.cpython-312.pyc and b/app/api/__pycache__/main.cpython-312.pyc differ
|
|
|
app/api/main.py
CHANGED
|
@@ -1,52 +1,101 @@
|
|
| 1 |
from fastapi import FastAPI, HTTPException
|
| 2 |
from pydantic import BaseModel
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
from app.model.loader import model_instance
|
|
|
|
| 5 |
|
| 6 |
# 1. Inizializziamo l'app
|
| 7 |
app = FastAPI(
|
| 8 |
title="Reputation Monitor API",
|
| 9 |
-
description="API per l'analisi del sentiment della reputazione aziendale",
|
| 10 |
version="1.0.0"
|
| 11 |
)
|
| 12 |
|
| 13 |
-
#
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
sentiment: str
|
| 21 |
confidence: float
|
| 22 |
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
|
|
|
|
|
|
| 27 |
|
| 28 |
-
# 4. Endpoint di Health Check (fondamentale per MLOps e Docker)
|
| 29 |
-
# Serve a capire se il container Γ¨ vivo
|
| 30 |
@app.get("/health")
|
| 31 |
def health_check():
|
| 32 |
# VERIFICA: Controlliamo se il modello Γ¨ stato caricato in memoria
|
| 33 |
if model_instance.model is not None:
|
| 34 |
return {"status": "ok", "model_loaded": True}
|
| 35 |
-
|
| 36 |
-
# Se il modello non c'Γ¨, restituiamo errore 503 (Service Unavailable)
|
| 37 |
-
raise HTTPException(status_code=503, detail="Model not loaded yet")
|
| 38 |
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
try:
|
| 43 |
-
#
|
| 44 |
-
|
| 45 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
return {
|
| 47 |
-
"
|
| 48 |
-
"
|
|
|
|
| 49 |
}
|
|
|
|
| 50 |
except Exception as e:
|
| 51 |
# Se qualcosa va storto, restituiamo errore 500
|
| 52 |
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
| 1 |
from fastapi import FastAPI, HTTPException
|
| 2 |
from pydantic import BaseModel
|
| 3 |
+
from typing import List
|
| 4 |
+
import csv
|
| 5 |
+
import os
|
| 6 |
+
from datetime import datetime
|
| 7 |
+
|
| 8 |
+
# Importiamo l'istanza del modello e l'istanza di GoogleNews per ricerca news
|
| 9 |
from app.model.loader import model_instance
|
| 10 |
+
from app.services.news_client import news_instance # Assicurati che il file si chiami news_client.py
|
| 11 |
|
| 12 |
# 1. Inizializziamo l'app
|
| 13 |
app = FastAPI(
|
| 14 |
title="Reputation Monitor API",
|
| 15 |
+
description="API per l'analisi del sentiment della reputazione aziendale sulla base di news estrapolate con Google di un azienda/soggetto dato in input",
|
| 16 |
version="1.0.0"
|
| 17 |
)
|
| 18 |
|
| 19 |
+
# --- MONITORAGGIO (Logging su CSV) ---
|
| 20 |
+
LOG_FILE = "reputation_logs.csv"
|
| 21 |
+
if not os.path.exists(LOG_FILE):
|
| 22 |
+
with open(LOG_FILE, mode='w', newline='', encoding='utf-8') as file:
|
| 23 |
+
writer = csv.writer(file)
|
| 24 |
+
writer.writerow(["timestamp", "query", "text", "sentiment", "confidence"])
|
| 25 |
+
|
| 26 |
+
def log_prediction(query, text, sentiment, confidence):
|
| 27 |
+
with open(LOG_FILE, mode='a', newline='', encoding='utf-8') as file:
|
| 28 |
+
writer = csv.writer(file)
|
| 29 |
+
writer.writerow([datetime.now(), query, text, sentiment, confidence])
|
| 30 |
|
| 31 |
+
# --- MODELLI DATI (Pydantic) ---
|
| 32 |
+
class AnalysisRequest(BaseModel):
|
| 33 |
+
query: str # Es. "Tesla"
|
| 34 |
+
limit: int = 5
|
| 35 |
+
|
| 36 |
+
class SingleResult(BaseModel):
|
| 37 |
+
text: str
|
| 38 |
sentiment: str
|
| 39 |
confidence: float
|
| 40 |
|
| 41 |
+
class AnalysisResponse(BaseModel):
|
| 42 |
+
query: str
|
| 43 |
+
results: List[SingleResult]
|
| 44 |
+
summary: dict # Es. {"positive": 3, "negative": 1}
|
| 45 |
+
|
| 46 |
+
# --- ENDPOINTS ---
|
| 47 |
|
|
|
|
|
|
|
| 48 |
@app.get("/health")
|
| 49 |
def health_check():
|
| 50 |
# VERIFICA: Controlliamo se il modello Γ¨ stato caricato in memoria
|
| 51 |
if model_instance.model is not None:
|
| 52 |
return {"status": "ok", "model_loaded": True}
|
| 53 |
+
raise HTTPException(status_code=503, detail="Model not loaded")
|
|
|
|
|
|
|
| 54 |
|
| 55 |
+
@app.post("/analyze", response_model=AnalysisResponse)
|
| 56 |
+
def analyze_company(request: AnalysisRequest):
|
| 57 |
+
# --- DEBUG PRINT ---
|
| 58 |
+
print(f"π₯ API RECEIVED REQUEST -> Query: {request.query}, Limit: {request.limit}")
|
| 59 |
+
# -------------------
|
| 60 |
+
"""
|
| 61 |
+
1. Cerca news su Google
|
| 62 |
+
2. Analizza il sentiment di ogni news
|
| 63 |
+
3. Salva i log
|
| 64 |
+
4. Restituisce il report completo
|
| 65 |
+
"""
|
| 66 |
try:
|
| 67 |
+
# 1. Scarica le News
|
| 68 |
+
news_texts = news_instance.search_news(request.query, limit=request.limit)
|
| 69 |
|
| 70 |
+
if not news_texts:
|
| 71 |
+
return {"query": request.query, "results": [], "summary": {}}
|
| 72 |
+
|
| 73 |
+
analyzed_results = []
|
| 74 |
+
sentiment_counts = {"positive": 0, "neutral": 0, "negative": 0}
|
| 75 |
+
|
| 76 |
+
# 2. Analizza ogni notizia col Modello
|
| 77 |
+
for text in news_texts:
|
| 78 |
+
sentiment, confidence = model_instance.predict(text)
|
| 79 |
+
|
| 80 |
+
# Aggiorna conteggi
|
| 81 |
+
sentiment_counts[sentiment] += 1
|
| 82 |
+
|
| 83 |
+
# Aggiungi alla lista
|
| 84 |
+
analyzed_results.append({
|
| 85 |
+
"text": text,
|
| 86 |
+
"sentiment": sentiment,
|
| 87 |
+
"confidence": confidence
|
| 88 |
+
})
|
| 89 |
+
|
| 90 |
+
# 3. Logga per il monitoraggio (Punto 2 dell'esercizio)
|
| 91 |
+
log_prediction(request.query, text, sentiment, confidence)
|
| 92 |
+
|
| 93 |
return {
|
| 94 |
+
"query": request.query,
|
| 95 |
+
"results": analyzed_results,
|
| 96 |
+
"summary": sentiment_counts
|
| 97 |
}
|
| 98 |
+
|
| 99 |
except Exception as e:
|
| 100 |
# Se qualcosa va storto, restituiamo errore 500
|
| 101 |
raise HTTPException(status_code=500, detail=str(e))
|
app/services/__pycache__/news_client.cpython-312.pyc
ADDED
|
Binary file (2.26 kB). View file
|
|
|
app/services/news_client.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from GoogleNews import GoogleNews
|
| 2 |
+
|
| 3 |
+
class NewsClient:
|
| 4 |
+
def __init__(self):
|
| 5 |
+
# Configura Google News (Lingua Inglese, ultimi 7 giorni)
|
| 6 |
+
self.googlenews = GoogleNews(lang='en', period='7d')
|
| 7 |
+
print("β
Google News Client initialized")
|
| 8 |
+
|
| 9 |
+
def search_news(self, query: str, limit: int = 10):
|
| 10 |
+
"""
|
| 11 |
+
Cerca news iterando sulle pagine finchΓ© non raggiunge il 'limit'.
|
| 12 |
+
"""
|
| 13 |
+
print(f"π Searching News for: {query} (Target: {limit})...")
|
| 14 |
+
self.googlenews.clear()
|
| 15 |
+
|
| 16 |
+
# 1. Prima ricerca (Pagina 1)
|
| 17 |
+
self.googlenews.search(query)
|
| 18 |
+
all_results = self.googlenews.result()
|
| 19 |
+
|
| 20 |
+
# 2. Se non bastano, scarichiamo le pagine successive
|
| 21 |
+
page = 2
|
| 22 |
+
while len(all_results) < limit:
|
| 23 |
+
print(f" ... Fetching page {page} to reach limit ...")
|
| 24 |
+
self.googlenews.getpage(page)
|
| 25 |
+
new_results = self.googlenews.result()
|
| 26 |
+
|
| 27 |
+
# Se la pagina nuova non aggiunge nulla, ci fermiamo (fine notizie)
|
| 28 |
+
if len(new_results) == len(all_results):
|
| 29 |
+
break
|
| 30 |
+
|
| 31 |
+
all_results = new_results
|
| 32 |
+
page += 1
|
| 33 |
+
|
| 34 |
+
# Safety break: Non andiamo oltre pagina 5 per non bloccare tutto
|
| 35 |
+
if page > 5:
|
| 36 |
+
break
|
| 37 |
+
|
| 38 |
+
# 3. Estrazione e Pulizia
|
| 39 |
+
texts_found = []
|
| 40 |
+
# Prendiamo un po' piΓΉ risultati del necessario per compensare quelli vuoti
|
| 41 |
+
for news in all_results:
|
| 42 |
+
full_text = f"{news['title']}. {news['desc']}"
|
| 43 |
+
|
| 44 |
+
# Pulizia base: evitiamo stringhe vuote o troppo corte
|
| 45 |
+
if len(full_text) > 20:
|
| 46 |
+
texts_found.append(full_text)
|
| 47 |
+
|
| 48 |
+
# Se abbiamo raggiunto il numero richiesto, ci fermiamo
|
| 49 |
+
if len(texts_found) >= limit:
|
| 50 |
+
break
|
| 51 |
+
|
| 52 |
+
print(f"β
Found {len(texts_found)} news items for '{query}'")
|
| 53 |
+
return texts_found
|
| 54 |
+
|
| 55 |
+
news_instance = NewsClient()
|
reputation_logs.csv
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
timestamp,query,text,sentiment,confidence
|
| 2 |
+
2025-12-11 23:33:12.555398,Elettromedia Spa,"Kia EV6 Owner Discovers Factory Wiring Error in Subwoofer, Fixes Weak Sound in Minutes. The EV6's weak sound system has frustrated owners, but reversing a few wires running to the Meridian subwoofer is all it takes to boost bass dramatically.",negative,0.6096141934394836
|
| 3 |
+
2025-12-11 23:33:12.740590,Elettromedia Spa,"Automotive Audio Speakers Market to Grow by USD 9.02 Billion (2024-2028) with Cost-Effective Aftermarket Speakers Boosting the Market, Report on Market Evolution Powered by AI - Technavio. Report on how AI is driving market transformation - The global automotive audio speakers market size is estimated to grow by USD 9.02 billion from 2024-2028...",positive,0.6643907427787781
|
| 4 |
+
2025-12-11 23:33:12.872823,Elettromedia Spa,"Lavoce Italiana Comes to North America. Italian transducer company Lavoce Italiana has established its first U.S. division, Elettromedia Corporation, based in Old Hickory, TN.",neutral,0.7901442050933838
|
| 5 |
+
2025-12-11 23:33:13.005912,Elettromedia Spa,"Elettromedia Training at KnowledgeFest. Press Release: Irvine, CA β Elettromedia USA is hosting several training sessions at KnowledgeFest Las Vegas February 3rd-5th.",neutral,0.8425894975662231
|
| 6 |
+
2025-12-11 23:40:24.248518,Ferrari,Ferrari crash: Luxury car had a history of speeding. Kolkata: Kolkata Police will seek help of engineers from Ferrari to study the reasons that led to one of their cars getting involved in an accident on.,negative,0.62410968542099
|
| 7 |
+
2025-12-11 23:40:24.383238,Ferrari,"Adrian Newey or Max Verstappen: what would Ferrariβs former key figures choose?. Maurizio Arrivabene and Luigi Mazzola reveal whether Ferrari should choose Adrian Newey or Max Verstappen, highlighting why technical excellence is more...",neutral,0.9174836277961731
|
| 8 |
+
2025-12-11 23:40:24.498802,Ferrari,60 Years Of The Dino: How Ferrari Turned A βJuniorβ Into A Legend. Ferrari's new feature on the Dino's 60th anniversary reveals how its first mid-engined V6 road car was hand-shaped into an icon that is now a blue-chip...,neutral,0.566238284111023
|
| 9 |
+
2025-12-11 23:40:24.606439,Ferrari,Former Ferrari boss weighs in on departing engine specialists: what Maranello should worry about. Maurizio Arrivabene discusses the departure of key Ferrari engine specialists and the potential impact on the team's 2026 F1 season as rivals prepare for...,neutral,0.9085403680801392
|
| 10 |
+
2025-12-11 23:40:24.708324,Ferrari,"Lewis Hamilton warned his F1 legacy is at risk after disastrous first season with Ferrari. Lewis Hamilton faces growing concerns about damaging his Formula 1 legacy after a difficult first season with Ferrari, as his ongoing struggles and lack of...",negative,0.7664820551872253
|
| 11 |
+
2025-12-11 23:40:24.824364,Ferrari,"While confident he made the most of what Ferrari gave him this year, Leclerc hopes for a lot more from 2026. After Ferrari's promising form of 2024 faded in 2025, Charles Leclerc is staying cautious about the team's chances of a quick rebound under the incoming...",positive,0.6923292875289917
|
| 12 |
+
2025-12-11 23:40:24.913603,Ferrari,"Bookies open betting on Lewis Hamilton retiring after nightmare season at Ferrari. When Lewis Hamilton announced he was joining Ferrari for the 2025 season, everything seemed to be lined up for a title charge.",neutral,0.7515650391578674
|
| 13 |
+
2025-12-11 23:40:25.015110,Ferrari,"Representing Ferrari, Alba Larsen is living a ""dream come true"".. Every racing driver dreams of racing in the famous Ferrari red, and at just 16 years old, Alba Larsen is about to achieve this feat.",positive,0.9591248035430908
|
| 14 |
+
2025-12-11 23:40:25.173862,Ferrari,"Ferrari: Fred Vasseur cautions Lewis Hamilton and Charles Leclerc about a key pitfall ahead of 2026 F1 campaign. Ferrari enter the 2026 Formula 1 season under huge pressure, with Lewis Hamilton and Charles Leclerc urged to push the team relentlessly rather than offer...",neutral,0.897258996963501
|
| 15 |
+
2025-12-11 23:40:25.289483,Ferrari,"""Lewis Hamilton isn't finished"": Former Ferrari boss tries to knock 'common sense' into the team's driver decisions. Former Ferrari F1 team principal Maurizio Arrivabene has lent support to Lewis Hamilton after his nightmare first season with the Scuderia.",neutral,0.681147038936615
|
| 16 |
+
2025-12-11 23:40:25.392379,Ferrari,"Lewis Hamiltonβs dire first Ferrari season keeps drawing the same comparison to Michael Schumacher. Lewis Hamilton joining Ferrari sparked pandemonium in Maranello ahead of the 2025 F1 season, but bitter disappointment and even anger quickly replaced any...",negative,0.7126820683479309
|
| 17 |
+
2025-12-11 23:40:25.496356,Ferrari,Lewis Hamilton urged to study Max Verstappenβs radio craft to help drive Ferrari forward. Lewis Hamilton is urged to adopt Max Verstappen's motivational style as Ferrari seek stronger leadership after a difficult 2025 F1 season.,neutral,0.6595488786697388
|
| 18 |
+
2025-12-11 23:40:25.642155,Ferrari,Recon 2022 Ferrari Roma 3.9 Coupe SURROUND VIEW / CARBON / JBL / ELECTRIC SEAT / INCOMING STOCK. 2022 Ferrari Roma 3.9 Coupe SURROUND VIEW / CARBON / JBL / ELECTRIC SEAT / INCOMING STOCK. Find all the best used / second hand and new cars from trusted...,neutral,0.69819575548172
|
| 19 |
+
2025-12-11 23:40:25.788479,Ferrari,"Ghost in white: a nearly-new, 986 HP Ferrari SF90 Stradale hits the market. This impeccably specced SF90 Stradale comes with a clean Carfax report and is now ready to find a new home. Ferrari SF90 Stradale.",positive,0.8663274645805359
|
| 20 |
+
2025-12-11 23:40:25.889634,Ferrari,Lewis Hamilton gets response from Ferrari after joint complaints with Charles Leclerc. Lewis Hamilton and Charles Leclerc spent the Formula 1 season complaining about how uncompetitive their Ferrari cars were compared to their championship...,neutral,0.6231443285942078
|
| 21 |
+
2025-12-11 23:40:26.000135,Ferrari,"214-Mile 2025 Ferrari 12Cilindri For Sale. Grab the keys to Ferrari's latest and greatest supercar! While most Ferraris have historically been recognized for their mid-engine configurations, Enzo.",positive,0.797694981098175
|
| 22 |
+
2025-12-11 23:40:26.115016,Ferrari,"Dubai Police Force Commits Crime Against Sanity With Mansory Purosangue. Dubai's police force is all about putting on a show. Its fleet is legendary for its collection of supercars, hypercars, and even four-rotor flying...",neutral,0.5327199101448059
|
| 23 |
+
2025-12-11 23:40:26.236435,Ferrari,"Carlos Sainz silences Williams βdoubtsβ after Ferrari departure. Carlos Sainz admitted ""there were doubts"" from outside about his move to Williams in 2025, but he believes the team is on an upward trend.",neutral,0.8175623416900635
|
| 24 |
+
2025-12-11 23:40:26.336150,Ferrari,"Europe cracks down on rule-breaking Ferrari, Rolls-Royce owners. Authorities in London and Vienna recently took these rich dweebs down a peg and let them know they can't just do whatever they want.",negative,0.7607141733169556
|
| 25 |
+
2025-12-11 23:40:26.446514,Ferrari,Lewis Hamilton's Dad may have just set up Ferrari star's F1 retirement. Seven-time F1 world champion Lewis Hamilton may just have been given a retirement plan from his father Anthony Hamilton.,neutral,0.5159865617752075
|
| 26 |
+
2025-12-11 23:40:44.616365,Elettromedia,Elettromedia USA Names New President. Rob Wempe to President of Mobile Audio for Advanced Marketing companies. Wempe was previously VP of Sales & Marketing at the company for fifteen years.,neutral,0.7228948473930359
|
| 27 |
+
2025-12-11 23:40:44.727524,Elettromedia,"Elettromedia USA Parent Buys Biketronics. Advanced Marketing, the parent company of Elettromedia USA, American Hard Bag and Velocity brands is proud to announce the acquisition of Biketronics located...",positive,0.8176090717315674
|
| 28 |
+
2025-12-11 23:40:44.818475,Elettromedia,"Elettromedia Elevated in MESA Role. Elettromedia, takes an elevated position as a vendor for the car audio Mobile Electronics Specialists of America buying group.",positive,0.6863150596618652
|
| 29 |
+
2025-12-11 23:40:44.936782,Elettromedia,"Elettromedia Adds Automated DSP Tuning. Elettromedia Adds Automated DSP Tuning ... Elettromedia, maker of Hertz and Audison, is offering the next generation of its tuning software, bit Drive 2.0. It...",neutral,0.5924315452575684
|
| 30 |
+
2025-12-11 23:40:45.039521,Elettromedia,"Elettromedia USA Owner Buys This Company. Elettromedia USA Owner Buys This Company ... Advanced Marketing, the US Distributor of Hertz, Audison and other brands, announced it is taking over the operations...",neutral,0.7651730179786682
|
| 31 |
+
2025-12-11 23:40:45.137103,Elettromedia,"Former JL Exec Joins Hertz/Audison Marine. Elettromedia announced the appointment of Brian Power as Vice President of its Marine Division, which includes both the Hertz and Audison marine audio...",neutral,0.8856467008590698
|
| 32 |
+
2025-12-11 23:40:45.252198,Elettromedia,"Elettromedia Names New Rep. Press Release (UNEDITED): IRVINE, CAβ Elettromedia-USA, manufacturer of premium electronics and speaker systems for the automotive, marine, and motorsports...",neutral,0.9273980855941772
|
| 33 |
+
2025-12-11 23:40:45.397305,Elettromedia,"Lavoce Italiana Comes to North America. Italian transducer company Lavoce Italiana has established its first U.S. division, Elettromedia Corporation, based in Old Hickory, TN.",neutral,0.7901442050933838
|
| 34 |
+
2025-12-11 23:40:45.510152,Elettromedia,Audison Announces Wide Availability of Forza Amps. Press Release (UNEDITED): LAS VEGASβ Audison announces wide availability for award-winning Forza DSP amplifiers. βAfter a very successful launch of AF Forza...,positive,0.5248441100120544
|
| 35 |
+
2025-12-11 23:40:45.602417,Elettromedia,"Elettromedia Training at KnowledgeFest. Press Release: Irvine, CA β Elettromedia USA is hosting several training sessions at KnowledgeFest Las Vegas February 3rd-5th.",neutral,0.8425894975662231
|
| 36 |
+
2025-12-11 23:40:45.700735,Elettromedia,Elettromedia Names Reps of the Year 2021. Press Release: Elettromedia is proud to announce the following reps and rep firms that were named Elettromedia Reps of the Year for 2021.,positive,0.9222844839096069
|
| 37 |
+
2025-12-11 23:40:45.802643,Elettromedia,"Elettromedia Hosts Reps in Italy. Press Release: Elettromedia will be conducting a sales rep training in Potenza Picena, Italy in March. Forty USA field reps will be trained on new products,...",neutral,0.8989458084106445
|
| 38 |
+
2025-12-11 23:40:45.897368,Elettromedia,"Elettromedia Appoints New Executive. Effectively immediately, Pilgrim will work with dealers to supply marketing support and materials and help drive sales. He will also keep a hand technical...",neutral,0.7104781270027161
|
| 39 |
+
2025-12-11 23:40:45.998556,Elettromedia,Advanced Marketing Exits Full Line 12 Volt Distribution. Advanced Marketing (AM) announced it is ceasing its full-line car audio wholesale distribution business. Instead it will devote its full efforts to growing...,neutral,0.7373328804969788
|
| 40 |
+
2025-12-11 23:40:46.081800,Elettromedia,"Elettromedia Names New Rep. Elettromedia has appointed Current Marketing as its representative for the Arizona and southern Nevada territories, effective immediately.",neutral,0.9036652445793152
|
| 41 |
+
2025-12-11 23:40:46.176913,Elettromedia,Elettromedia Promotes Wempe and Delgado. Elettromedia-USA has promoted Rob Wempe to Vice President of the company from his former post as Director of Sales and Marketing.,neutral,0.4994317293167114
|
| 42 |
+
2025-12-11 23:40:46.264677,Elettromedia,"Larry Frederick Leaves Elettromedia. Prominent, veteran car audio product developer Larry Frederick has left Elettromedia and is entertaining other possibilities in car electronics.",neutral,0.5087681412696838
|
| 43 |
+
2025-12-11 23:42:09.006472,Celaschi,Charleroi will pursue funding from restitution in Ha case. Councilman Larry Celaschi said the borough was harmed by the failure to pay taxes on illegal aliens.,negative,0.6671392917633057
|
| 44 |
+
2025-12-11 23:42:09.109556,Celaschi,"Charleroi councilman claims glass plant will reopen, but other officials cannot confirm. The closed Corelle Brands plant in Charleroi, shown on Friday. After it closed in April, production was moved to Lancaster, Ohio.",neutral,0.85775226354599
|
| 45 |
+
2025-12-11 23:42:09.233219,Celaschi,"Perryopolis man held for court in vehicular death case. John Delbert Celaschi Jr., 25, of Perryopolis, was held for court on all but one charge filed against him in Fayette County in relation to the vehicular...",neutral,0.715713381767273
|
| 46 |
+
2025-12-11 23:42:09.340242,Celaschi,"Immigrants Rebuilt a Pennsylvania Town β Then Became Targets. Larry Celaschi summons me to look at his cell phone, which displays a photo of a truck. The picture, which someone shared with Celaschi, features the...",neutral,0.945743203163147
|
| 47 |
+
2025-12-11 23:42:09.465843,Celaschi,"Andrew Celaschi Obituary (2025) - Carmichaels, PA - Observer-Reporter. Andrew Celaschi Obituary. Andrew Michael Celaschi, 25, died Tuesday, April 29, 2025, from injuries sustained when he was struck by a tree...",neutral,0.7802605032920837
|
| 48 |
+
2025-12-11 23:42:09.576318,Celaschi,"Greene man killed Tuesday when tree falls on car. Andrew Celaschi, 25, was the passenger in a Toyota Corolla that was headed westbound on Jefferson Road in Franklin Township, about 150 feet north of the...",negative,0.5343117713928223
|
| 49 |
+
2025-12-11 23:42:09.678246,Celaschi,"Perryopolis man charged with homicide by vehicle. A Perryopolis man faces homicide by vehicle while driving under the influence charges following a two-vehicle head-on crash Sept. 21, 2024.",negative,0.7082436680793762
|
| 50 |
+
2025-12-11 23:42:09.781644,Celaschi,"Fayette County man charged with vehicular homicide in crash that killed Ringgold grad. John Celaschi, 25, was leaving a wedding when the truck he was driving allegedly crossed the center line on Brownsville Road in Jefferson Township, hitting a...",negative,0.6681841015815735
|
| 51 |
+
2025-12-11 23:42:09.877497,Celaschi,"Perryopolis man charged with homicide by vehicle. A Perryopolis man faces homicide by vehicle while driving under the influence charges following a two-vehicle head-on accident on Sept. 21, 2024.",negative,0.695264458656311
|
| 52 |
+
2025-12-11 23:42:09.992874,Celaschi,"Victor Celaschi. He is survived by his beloved wife of 67 years, Alberta E. Celaschi (nee: Miller); his children, Victoria Aring (Lee) of Avon, and Victor Celaschi (Marie) of...",neutral,0.6017436981201172
|
| 53 |
+
2025-12-11 23:42:10.093366,Celaschi,"Bearcats grapplers pin down βDores. Bentworth dug deep to secure a hard-fought 42-26 victory over Frazier Wednesday night, using timely wins, clutch performances and key forfeits to pull away...",neutral,0.5737037062644958
|
| 54 |
+
2025-12-11 23:42:10.183541,Celaschi,"Charleroi council president statement draws ire of fellow council member. Councilman Larry Celaschi expressed his displeasure, taking issue with a statement concerning the plant property and its possible availability.",negative,0.6969603896141052
|
| 55 |
+
2025-12-11 23:42:10.289656,Celaschi,"Donora man killed in head-on collision in Fayette County. Quinte Lamar Brown, 26, of Donora, was traveling in the 800 block of Brownsville Road in Jefferson Township shortly before 11 pm when a vehicle being driven by...",neutral,0.5457555055618286
|
| 56 |
+
2025-12-11 23:42:10.385365,Celaschi,"Donora man dies in Fayette County crash. Troopers said Quinte Lamar Brown, 26, was driving a Chevrolet Cobalt on Brownsville Road in Jefferson Township just before 11 pm when an oncoming Chevrolet...",neutral,0.588758647441864
|
| 57 |
+
2025-12-11 23:42:10.478229,Celaschi,Efforts underway to save Charleroi plant. The company announced last week that it plans to shut down the facility by the end of the year. It's not over until it's over.,neutral,0.5509985089302063
|
| 58 |
+
2025-12-11 23:42:10.611811,Celaschi,"Tracy Lynn Herrada Obituary - Wolverine Lake , MI (1962-2024). Tracy Lynn Herrada (Celaschi) OBITUARY. Beloved wife of 20 years to Randall Herrada. Loving mother of Michelle (Abraham) Handgis, Jennifer (Stan) Sebastian,...",neutral,0.5667412877082825
|
| 59 |
+
2025-12-11 23:42:10.714440,Celaschi,"Linda Bailey Obituary (2023) - Carmichaels, PA - Observer-Reporter. Linda Joyce Bailey, 77, of Rices Landing, died Thursday, October 12, 2023, in Premier Care, Washington.",neutral,0.7752928137779236
|
| 60 |
+
2025-12-11 23:42:10.829393,Celaschi,"Claire Elizabeth Kolt Obituary. In Loving Memory of Claire Elizabeth Kolt A Bright Light Taken Too Soon It is with heavy hearts that we mourn the loss of our beloved Claire, a...",neutral,0.47717931866645813
|
| 61 |
+
2025-12-11 23:42:10.923217,Celaschi,"Clearview FCU's former CEO at helm of credit union in Chambersburg. Ron Celaschi, president and CEO at Clearview FCU for five years, joined Patriot FCU last month.",neutral,0.9147176742553711
|
| 62 |
+
2025-12-11 23:42:11.007703,Celaschi,"Patriot Federal Credit Union in Pennsylvania names new CEO. Ronald Celaschi will succeed Brad Warner, who announced his intention to retire last year.",neutral,0.8869218826293945
|
requirements.txt
CHANGED
|
@@ -1,19 +1,16 @@
|
|
| 1 |
-
# --- Web API ---
|
| 2 |
fastapi==0.109.0
|
| 3 |
uvicorn[standard]==0.27.0
|
| 4 |
pydantic==2.6.0
|
| 5 |
-
|
| 6 |
-
# --- Machine Learning (Base) ---
|
| 7 |
-
# Per ora mettiamo scikit-learn/pandas per testare,
|
| 8 |
-
# poi aggiungeremo transformers/torch se usiamo RoBERTa
|
| 9 |
-
pandas
|
| 10 |
-
scikit-learn
|
| 11 |
-
joblib
|
| 12 |
torch
|
| 13 |
transformers
|
| 14 |
scipy
|
| 15 |
numpy
|
| 16 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
# --- Testing ---
|
| 18 |
pytest
|
| 19 |
httpx==0.27.0
|
|
|
|
|
|
|
| 1 |
fastapi==0.109.0
|
| 2 |
uvicorn[standard]==0.27.0
|
| 3 |
pydantic==2.6.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
torch
|
| 5 |
transformers
|
| 6 |
scipy
|
| 7 |
numpy
|
| 8 |
+
# --- Scraping ---
|
| 9 |
+
GoogleNews
|
| 10 |
+
# --- Frontend ---
|
| 11 |
+
streamlit
|
| 12 |
+
requests
|
| 13 |
+
pandas
|
| 14 |
# --- Testing ---
|
| 15 |
pytest
|
| 16 |
httpx==0.27.0
|
streamlit_app/app.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import requests
|
| 3 |
+
import pandas as pd
|
| 4 |
+
import time
|
| 5 |
+
|
| 6 |
+
# Configurazione Pagina
|
| 7 |
+
st.set_page_config(page_title="Reputation Monitor", page_icon="π", layout="wide")
|
| 8 |
+
|
| 9 |
+
st.title("π AI Reputation Monitor")
|
| 10 |
+
st.markdown("Monitor brand reputation using **Google News** and **RoBERTa AI**.")
|
| 11 |
+
|
| 12 |
+
# URL dell'API (Se siamo in Docker usa localhost, altrimenti l'URL dello Space)
|
| 13 |
+
# In Codespaces locale usa questo:
|
| 14 |
+
API_URL = "http://localhost:8000"
|
| 15 |
+
|
| 16 |
+
# --- SIDEBAR ---
|
| 17 |
+
with st.sidebar:
|
| 18 |
+
st.header("βοΈ Configuration")
|
| 19 |
+
target_company = st.text_input("Company/Brand to monitor:", value="Ferrari")
|
| 20 |
+
num_news = st.slider("Number of news to analyze:", 1, 20, 5)
|
| 21 |
+
analyze_btn = st.button("π Analyze Reputation")
|
| 22 |
+
st.divider()
|
| 23 |
+
st.info("System Status: Online π’")
|
| 24 |
+
|
| 25 |
+
# --- MAIN LOGIC ---
|
| 26 |
+
if analyze_btn:
|
| 27 |
+
if target_company:
|
| 28 |
+
with st.spinner(f"π Searching news for '{target_company}' and analyzing sentiment..."):
|
| 29 |
+
try:
|
| 30 |
+
# Chiamata all'API
|
| 31 |
+
payload = {"query": target_company, "limit": num_news}
|
| 32 |
+
response = requests.post(f"{API_URL}/analyze", json=payload)
|
| 33 |
+
|
| 34 |
+
if response.status_code == 200:
|
| 35 |
+
data = response.json()
|
| 36 |
+
results = data['results']
|
| 37 |
+
summary = data['summary']
|
| 38 |
+
|
| 39 |
+
# 1. METRICHE (KPI)
|
| 40 |
+
col1, col2, col3 = st.columns(3)
|
| 41 |
+
col1.metric("Positive News", summary.get('positive', 0), delta_color="normal")
|
| 42 |
+
col2.metric("Negative News", summary.get('negative', 0), delta_color="inverse")
|
| 43 |
+
col3.metric("Neutral News", summary.get('neutral', 0), delta_color="off")
|
| 44 |
+
|
| 45 |
+
# 2. GRAFICI
|
| 46 |
+
st.subheader("Sentiment Distribution")
|
| 47 |
+
chart_data = pd.DataFrame({
|
| 48 |
+
"Sentiment": list(summary.keys()),
|
| 49 |
+
"Count": list(summary.values())
|
| 50 |
+
})
|
| 51 |
+
st.bar_chart(chart_data, x="Sentiment", y="Count", color="Sentiment")
|
| 52 |
+
|
| 53 |
+
# 3. DETTAGLI (Tabella)
|
| 54 |
+
st.subheader("Latest News Analyzed")
|
| 55 |
+
for item in results:
|
| 56 |
+
color = "green" if item['sentiment'] == "positive" else "red" if item['sentiment'] == "negative" else "gray"
|
| 57 |
+
with st.expander(f":{color}[{item['sentiment'].upper()}] - {item['text'][:80]}..."):
|
| 58 |
+
st.write(f"**Full Text:** {item['text']}")
|
| 59 |
+
st.write(f"**Confidence:** {item['confidence']:.2%}")
|
| 60 |
+
|
| 61 |
+
else:
|
| 62 |
+
st.error(f"Error {response.status_code}: {response.text}")
|
| 63 |
+
except Exception as e:
|
| 64 |
+
st.error(f"Connection Error: {e}. Is the API running?")
|
| 65 |
+
else:
|
| 66 |
+
st.warning("Please enter a company name.")
|
| 67 |
+
|
| 68 |
+
# --- MONITORING TAB (Punto 2 dell'esercizio) ---
|
| 69 |
+
st.divider()
|
| 70 |
+
st.header("π Continuous Monitoring Logs")
|
| 71 |
+
if st.button("Refresh Logs"):
|
| 72 |
+
try:
|
| 73 |
+
# Leggiamo il CSV generato dall'API
|
| 74 |
+
# Nota: Funziona perchΓ© in Codespaces condividiamo il file system.
|
| 75 |
+
# In produzione servirebbe un endpoint API dedicato /get_logs
|
| 76 |
+
if requests.get(f"{API_URL}/health").status_code == 200:
|
| 77 |
+
# Trucco: leggiamo il file locale (se siamo in locale)
|
| 78 |
+
try:
|
| 79 |
+
df_logs = pd.read_csv("reputation_logs.csv")
|
| 80 |
+
st.dataframe(df_logs.sort_values(by="timestamp", ascending=False).head(50))
|
| 81 |
+
except:
|
| 82 |
+
st.warning("Logs not accessible directly (Are you in Docker?)")
|
| 83 |
+
except:
|
| 84 |
+
st.warning("API not reachable")
|
test_scraper.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app.services.news_client import news_instance
|
| 2 |
+
|
| 3 |
+
azienda = "Elettromedia S.P.A."
|
| 4 |
+
notizie = news_instance.search_news(azienda, limit=5)
|
| 5 |
+
|
| 6 |
+
print(f"\n--- Ultime notizie su {azienda} ---")
|
| 7 |
+
for i, testo in enumerate(notizie):
|
| 8 |
+
print(f"[{i+1}] {testo}")
|
tests/test_api.py
CHANGED
|
@@ -30,6 +30,24 @@ def test_prediction_negative():
|
|
| 30 |
assert response.status_code == 200
|
| 31 |
assert response.json()["sentiment"] == "negative"
|
| 32 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
|
| 34 |
"""
|
| 35 |
scrivi sul terminale: pytest Serve per lanciare il test
|
|
|
|
| 30 |
assert response.status_code == 200
|
| 31 |
assert response.json()["sentiment"] == "negative"
|
| 32 |
|
| 33 |
+
def test_analyze_endpoint():
|
| 34 |
+
"""
|
| 35 |
+
Verifica che l'endpoint complesso /analyze risponda correttamente.
|
| 36 |
+
Nota: Questo test effettua una chiamata reale a Google News.
|
| 37 |
+
"""
|
| 38 |
+
payload = {"query": "Apple", "limit": 1}
|
| 39 |
+
response = client.post("/analyze", json=payload)
|
| 40 |
+
|
| 41 |
+
assert response.status_code == 200
|
| 42 |
+
data = response.json()
|
| 43 |
+
|
| 44 |
+
# Verifichiamo la struttura della risposta
|
| 45 |
+
assert "query" in data
|
| 46 |
+
assert "results" in data
|
| 47 |
+
assert "summary" in data
|
| 48 |
+
# Verifichiamo che summary abbia i conteggi
|
| 49 |
+
assert "positive" in data["summary"]
|
| 50 |
+
|
| 51 |
|
| 52 |
"""
|
| 53 |
scrivi sul terminale: pytest Serve per lanciare il test
|