|
@@ -9,6 +9,7 @@ from news_mcp.config import NEWS_REFRESH_INTERVAL_SECONDS, NEWS_BACKGROUND_REFRE
|
|
|
from news_mcp.jobs.poller import refresh_clusters
|
|
from news_mcp.jobs.poller import refresh_clusters
|
|
|
from news_mcp.storage.sqlite_store import SQLiteClusterStore
|
|
from news_mcp.storage.sqlite_store import SQLiteClusterStore
|
|
|
from news_mcp.enrichment.groq_enrich import summarize_cluster_groq
|
|
from news_mcp.enrichment.groq_enrich import summarize_cluster_groq
|
|
|
|
|
+from collections import Counter
|
|
|
|
|
|
|
|
|
|
|
|
|
mcp = FastMCP(
|
|
mcp = FastMCP(
|
|
@@ -182,6 +183,75 @@ async def detect_emerging_topics(limit: int = 10):
|
|
|
return emerging[:limit]
|
|
return emerging[:limit]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+@mcp.tool(description="What's the overall sentiment around an entity within a timeframe?")
|
|
|
|
|
+async def get_news_sentiment(entity: str, timeframe: str = "24h"):
|
|
|
|
|
+ store = SQLiteClusterStore(DB_PATH)
|
|
|
|
|
+
|
|
|
|
|
+ ent = str(entity).strip().lower()
|
|
|
|
|
+ if not ent:
|
|
|
|
|
+ return {
|
|
|
|
|
+ "entity": entity,
|
|
|
|
|
+ "sentiment": "neutral",
|
|
|
|
|
+ "score": 0.0,
|
|
|
|
|
+ "cluster_count": 0,
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ # timeframe: accept '24h' or '24'
|
|
|
|
|
+ tf = str(timeframe).strip().lower()
|
|
|
|
|
+ try:
|
|
|
|
|
+ hours = int(tf[:-1]) if tf.endswith("h") else int(tf)
|
|
|
|
|
+ except Exception:
|
|
|
|
|
+ hours = 24
|
|
|
|
|
+ hours = max(1, min(int(hours), 168))
|
|
|
|
|
+
|
|
|
|
|
+ clusters = store.get_latest_clusters_all_topics(ttl_hours=hours, limit=500)
|
|
|
|
|
+ matched = []
|
|
|
|
|
+ for c in clusters:
|
|
|
|
|
+ ents = c.get("entities") or []
|
|
|
|
|
+ if any(ent in str(e).lower() for e in ents):
|
|
|
|
|
+ matched.append(c)
|
|
|
|
|
+
|
|
|
|
|
+ if not matched:
|
|
|
|
|
+ return {
|
|
|
|
|
+ "entity": entity,
|
|
|
|
|
+ "sentiment": "neutral",
|
|
|
|
|
+ "score": 0.0,
|
|
|
|
|
+ "cluster_count": 0,
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ scores = []
|
|
|
|
|
+ labels = []
|
|
|
|
|
+ for c in matched:
|
|
|
|
|
+ s = c.get("sentimentScore")
|
|
|
|
|
+ if s is not None:
|
|
|
|
|
+ try:
|
|
|
|
|
+ scores.append(float(s))
|
|
|
|
|
+ except Exception:
|
|
|
|
|
+ pass
|
|
|
|
|
+ lbl = c.get("sentiment")
|
|
|
|
|
+ if lbl:
|
|
|
|
|
+ labels.append(str(lbl).lower())
|
|
|
|
|
+
|
|
|
|
|
+ avg_score = sum(scores) / len(scores) if scores else 0.0
|
|
|
|
|
+
|
|
|
|
|
+ # Majority vote on sentiment label, fall back to sign of avg score.
|
|
|
|
|
+ if labels:
|
|
|
|
|
+ majority = Counter(labels).most_common(1)[0][0]
|
|
|
|
|
+ if majority in {"positive", "negative", "neutral"}:
|
|
|
|
|
+ sentiment = majority
|
|
|
|
|
+ else:
|
|
|
|
|
+ sentiment = "positive" if avg_score > 0 else "negative" if avg_score < 0 else "neutral"
|
|
|
|
|
+ else:
|
|
|
|
|
+ sentiment = "positive" if avg_score > 0 else "negative" if avg_score < 0 else "neutral"
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ "entity": entity,
|
|
|
|
|
+ "sentiment": sentiment,
|
|
|
|
|
+ "score": round(avg_score, 3),
|
|
|
|
|
+ "cluster_count": len(matched),
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
app = FastAPI(title="News MCP Server")
|
|
app = FastAPI(title="News MCP Server")
|
|
|
|
|
|
|
|
app.mount("/mcp", mcp.sse_app())
|
|
app.mount("/mcp", mcp.sse_app())
|