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")
|
||||
await _check_incident_access(db, incident_id, current_user["id"], tenant_id)
|
||||
cursor = await db.execute(
|
||||
"""SELECT source,
|
||||
"""SELECT a.source,
|
||||
COUNT(*) AS article_count,
|
||||
GROUP_CONCAT(DISTINCT COALESCE(language,'de')) AS languages
|
||||
FROM articles WHERE incident_id = ?
|
||||
GROUP BY source ORDER BY article_count DESC""",
|
||||
GROUP_CONCAT(DISTINCT COALESCE(a.language,'de')) AS languages,
|
||||
COUNT(DISTINCT m.article_id) AS fimi_match_count
|
||||
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,),
|
||||
)
|
||||
sources = []
|
||||
@@ -507,6 +510,7 @@ async def get_articles_sources_summary(
|
||||
d = dict(r)
|
||||
langs = (d.pop("languages") or "de").split(",")
|
||||
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)
|
||||
src = d.get("source") or ""
|
||||
if src.startswith("X: "):
|
||||
@@ -532,6 +536,114 @@ async def get_articles_sources_summary(
|
||||
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")
|
||||
async def get_articles_timeline_buckets(
|
||||
incident_id: int,
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren