feat(fimi): Backend-Endpoints fimi-matches + fimi-summary, Match-Count in sources-summary
- GET /{id}/fimi-matches: Treffer gruppiert nach Artikel inkl. Provenienz
(Claim, Widerlegung, Case-URL, Zitat) fuer Andockpunkt 1.
- GET /{id}/fimi-summary: Aggregat (geprueft, Treffer, Narrative, Quellen)
fuer die Lagebild-Qualitaetsachse (Andockpunkt 3).
- sources-summary um fimi_match_count erweitert (Andockpunkt 2).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Dieser Commit ist enthalten in:
@@ -495,11 +495,14 @@ async def get_articles_sources_summary(
|
|||||||
tenant_id = current_user.get("tenant_id")
|
tenant_id = current_user.get("tenant_id")
|
||||||
await _check_incident_access(db, incident_id, current_user["id"], tenant_id)
|
await _check_incident_access(db, incident_id, current_user["id"], tenant_id)
|
||||||
cursor = await db.execute(
|
cursor = await db.execute(
|
||||||
"""SELECT source,
|
"""SELECT a.source,
|
||||||
COUNT(*) AS article_count,
|
COUNT(*) AS article_count,
|
||||||
GROUP_CONCAT(DISTINCT COALESCE(language,'de')) AS languages
|
GROUP_CONCAT(DISTINCT COALESCE(a.language,'de')) AS languages,
|
||||||
FROM articles WHERE incident_id = ?
|
COUNT(DISTINCT m.article_id) AS fimi_match_count
|
||||||
GROUP BY source ORDER BY article_count DESC""",
|
FROM articles a
|
||||||
|
LEFT JOIN article_fimi_matches m ON m.article_id = a.id
|
||||||
|
WHERE a.incident_id = ?
|
||||||
|
GROUP BY a.source ORDER BY article_count DESC""",
|
||||||
(incident_id,),
|
(incident_id,),
|
||||||
)
|
)
|
||||||
sources = []
|
sources = []
|
||||||
@@ -507,6 +510,7 @@ async def get_articles_sources_summary(
|
|||||||
d = dict(r)
|
d = dict(r)
|
||||||
langs = (d.pop("languages") or "de").split(",")
|
langs = (d.pop("languages") or "de").split(",")
|
||||||
d["languages"] = sorted({(l or "de").strip() for l in langs if l is not None})
|
d["languages"] = sorted({(l or "de").strip() for l in langs if l is not None})
|
||||||
|
d["fimi_match_count"] = d.get("fimi_match_count") or 0
|
||||||
# Quellentyp aus dem source-Praefix ableiten (fuer den Typ-Filter der Quellenuebersicht)
|
# Quellentyp aus dem source-Praefix ableiten (fuer den Typ-Filter der Quellenuebersicht)
|
||||||
src = d.get("source") or ""
|
src = d.get("source") or ""
|
||||||
if src.startswith("X: "):
|
if src.startswith("X: "):
|
||||||
@@ -532,6 +536,114 @@ async def get_articles_sources_summary(
|
|||||||
return {"total": total, "sources": sources, "language_counts": lang_counts}
|
return {"total": total, "sources": sources, "language_counts": lang_counts}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{incident_id}/fimi-matches")
|
||||||
|
async def get_fimi_matches(
|
||||||
|
incident_id: int,
|
||||||
|
current_user: dict = Depends(get_current_user),
|
||||||
|
db: aiosqlite.Connection = Depends(db_dependency),
|
||||||
|
):
|
||||||
|
"""FIMI-Treffer einer Lage, gruppiert nach Artikel (fuer Andockpunkt 1).
|
||||||
|
|
||||||
|
Pro Artikel die verlinkten EUvsDisinfo-Falschbehauptungen mit Provenienz:
|
||||||
|
Claim-Text, Widerlegung, Case-URL, Embedding-Score und das woertliche
|
||||||
|
Zitat aus dem Artikel. Der Monitor wertet nicht selbst, er verweist.
|
||||||
|
"""
|
||||||
|
tenant_id = current_user.get("tenant_id")
|
||||||
|
await _check_incident_access(db, incident_id, current_user["id"], tenant_id)
|
||||||
|
cursor = await db.execute(
|
||||||
|
"""SELECT m.article_id, m.fimi_claim_id, m.score, m.role, m.matched_text,
|
||||||
|
c.text AS claim_text, c.verdict, c.verdict_summary,
|
||||||
|
c.source_ref, c.case_url
|
||||||
|
FROM article_fimi_matches m
|
||||||
|
JOIN articles a ON a.id = m.article_id
|
||||||
|
JOIN fimi_claims c ON c.id = m.fimi_claim_id
|
||||||
|
WHERE a.incident_id = ?
|
||||||
|
ORDER BY m.score DESC""",
|
||||||
|
(incident_id,),
|
||||||
|
)
|
||||||
|
by_article: dict[str, list] = {}
|
||||||
|
for r in await cursor.fetchall():
|
||||||
|
d = dict(r)
|
||||||
|
aid = str(d["article_id"])
|
||||||
|
by_article.setdefault(aid, []).append({
|
||||||
|
"claim_id": d["fimi_claim_id"],
|
||||||
|
"claim_text": d["claim_text"],
|
||||||
|
"verdict": d["verdict"],
|
||||||
|
"verdict_summary": d["verdict_summary"],
|
||||||
|
"case_url": d["case_url"],
|
||||||
|
"source_ref": d["source_ref"],
|
||||||
|
"score": d["score"],
|
||||||
|
"passage": d["matched_text"],
|
||||||
|
})
|
||||||
|
return {"matches_by_article": by_article, "article_count": len(by_article)}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{incident_id}/fimi-summary")
|
||||||
|
async def get_fimi_summary(
|
||||||
|
incident_id: int,
|
||||||
|
current_user: dict = Depends(get_current_user),
|
||||||
|
db: aiosqlite.Connection = Depends(db_dependency),
|
||||||
|
):
|
||||||
|
"""Aggregierte FIMI-Kennzahlen fuer die Lagebild-Qualitaetsachse (Andockpunkt 3).
|
||||||
|
|
||||||
|
Antwortet auch sinnvoll, wenn noch nichts geprueft wurde."""
|
||||||
|
tenant_id = current_user.get("tenant_id")
|
||||||
|
await _check_incident_access(db, incident_id, current_user["id"], tenant_id)
|
||||||
|
|
||||||
|
cur = await db.execute(
|
||||||
|
"""SELECT COUNT(*) AS total,
|
||||||
|
SUM(CASE WHEN fimi_checked_at IS NOT NULL THEN 1 ELSE 0 END) AS checked
|
||||||
|
FROM articles WHERE incident_id = ?""",
|
||||||
|
(incident_id,),
|
||||||
|
)
|
||||||
|
row = await cur.fetchone()
|
||||||
|
total = row["total"] or 0
|
||||||
|
checked = row["checked"] or 0
|
||||||
|
|
||||||
|
cur = await db.execute(
|
||||||
|
"""SELECT COUNT(DISTINCT m.article_id) AS matched_articles,
|
||||||
|
COUNT(DISTINCT m.fimi_claim_id) AS distinct_claims
|
||||||
|
FROM article_fimi_matches m
|
||||||
|
JOIN articles a ON a.id = m.article_id
|
||||||
|
WHERE a.incident_id = ?""",
|
||||||
|
(incident_id,),
|
||||||
|
)
|
||||||
|
row = await cur.fetchone()
|
||||||
|
matched_articles = row["matched_articles"] or 0
|
||||||
|
distinct_claims = row["distinct_claims"] or 0
|
||||||
|
|
||||||
|
cur = await db.execute(
|
||||||
|
"""SELECT c.id AS claim_id, c.text AS claim_text, c.case_url,
|
||||||
|
COUNT(DISTINCT m.article_id) AS article_count
|
||||||
|
FROM article_fimi_matches m
|
||||||
|
JOIN articles a ON a.id = m.article_id
|
||||||
|
JOIN fimi_claims c ON c.id = m.fimi_claim_id
|
||||||
|
WHERE a.incident_id = ?
|
||||||
|
GROUP BY c.id ORDER BY article_count DESC LIMIT 10""",
|
||||||
|
(incident_id,),
|
||||||
|
)
|
||||||
|
top_claims = [dict(r) for r in await cur.fetchall()]
|
||||||
|
|
||||||
|
cur = await db.execute(
|
||||||
|
"""SELECT a.source, COUNT(DISTINCT m.article_id) AS match_count
|
||||||
|
FROM article_fimi_matches m
|
||||||
|
JOIN articles a ON a.id = m.article_id
|
||||||
|
WHERE a.incident_id = ?
|
||||||
|
GROUP BY a.source ORDER BY match_count DESC LIMIT 10""",
|
||||||
|
(incident_id,),
|
||||||
|
)
|
||||||
|
by_source = [dict(r) for r in await cur.fetchall()]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"articles_total": total,
|
||||||
|
"articles_checked": checked,
|
||||||
|
"articles_with_match": matched_articles,
|
||||||
|
"distinct_claims": distinct_claims,
|
||||||
|
"top_claims": top_claims,
|
||||||
|
"by_source": by_source,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{incident_id}/articles/timeline-buckets")
|
@router.get("/{incident_id}/articles/timeline-buckets")
|
||||||
async def get_articles_timeline_buckets(
|
async def get_articles_timeline_buckets(
|
||||||
incident_id: int,
|
incident_id: int,
|
||||||
|
|||||||
In neuem Issue referenzieren
Einen Benutzer sperren