fix(news): exclude null AI results from homepage reading inputs
Development CI / check-and-build (push) Failing after 1m23s
Development CI / deploy-development (push) Skipped
GitHub Git mirror / mirror (push) Successful in 6s

This commit is contained in:
eezd committed 2026-11-02 21:20:44 +08:00
1 parent 8f1d3b249b
commit be3cf45362
3 files changed
+41 -7

No files matched your search

+3 -3
View File
@@ -175,8 +175,8 @@ async def coverage(session):
async def overview(session: DB, hours: RiskWindow = RiskWindow.DAY): async def overview(session: DB, hours: RiskWindow = RiskWindow.DAY):
now = datetime.now(UTC); cutoff = now - timedelta(hours=hours) now = datetime.now(UTC); cutoff = now - timedelta(hours=hours)
counts = dict((await session.execute(select(NewsSummary.importance_stars, func.count()).where( counts = dict((await session.execute(select(NewsSummary.importance_stars, func.count()).where(
NewsSummary.updated_at >= cutoff, NewsSummary.result.is_not(None), NewsSummary.replacement_ids == [], NewsSummary.state != 'withdrawn').group_by(NewsSummary.importance_stars))).all()) NewsSummary.updated_at >= cutoff, func.jsonb_typeof(NewsSummary.result) == 'object', NewsSummary.replacement_ids == [], NewsSummary.state != 'withdrawn').group_by(NewsSummary.importance_stars))).all())
snapshot = await session.scalar(select(RiskSnapshot).where(RiskSnapshot.state == 'done', RiskSnapshot.result['inputs'].is_not(None)).order_by(RiskSnapshot.id.desc()).limit(1)) snapshot = await session.scalar(select(RiskSnapshot).where(RiskSnapshot.state == 'done', func.jsonb_typeof(RiskSnapshot.result['inputs']) == 'array').order_by(RiskSnapshot.id.desc()).limit(1))
last = await session.scalar(select(RiskSnapshot).order_by(RiskSnapshot.id.desc()).limit(1)) last = await session.scalar(select(RiskSnapshot).order_by(RiskSnapshot.id.desc()).limit(1))
scenario = None scenario = None
stage2 = dict((await session.execute(select(NewsSummary.state, func.count()).group_by(NewsSummary.state))).all()) stage2 = dict((await session.execute(select(NewsSummary.state, func.count()).group_by(NewsSummary.state))).all())
@@ -211,7 +211,7 @@ async def overview(session: DB, hours: RiskWindow = RiskWindow.DAY):
async def events(session: DB, hours: RiskWindow | None = None, stars: int = Query(0, ge=0, le=5), async def events(session: DB, hours: RiskWindow | None = None, stars: int = Query(0, ge=0, le=5),
tag: str = '', importance: int = Query(0, ge=0, le=5), category: str = '', asset: str = '', q: str = '', start: datetime | None = None, end: datetime | None = None, tag: str = '', importance: int = Query(0, ge=0, le=5), category: str = '', asset: str = '', q: str = '', start: datetime | None = None, end: datetime | None = None,
page: int = Query(1, ge=1), limit: int = Query(20, ge=1, le=100)): page: int = Query(1, ge=1), limit: int = Query(20, ge=1, le=100)):
conditions = [NewsSummary.replacement_ids == [], NewsSummary.state != 'withdrawn', NewsSummary.stars >= stars, NewsSummary.result.is_not(None)] conditions = [NewsSummary.replacement_ids == [], NewsSummary.state != 'withdrawn', NewsSummary.stars >= stars, func.jsonb_typeof(NewsSummary.result) == 'object']
if importance: conditions.append(NewsSummary.importance_stars >= importance) if importance: conditions.append(NewsSummary.importance_stars >= importance)
if tag: conditions.append(NewsSummary.tags.contains([tag.strip().casefold()])) if tag: conditions.append(NewsSummary.tags.contains([tag.strip().casefold()]))
if hours: conditions.append(NewsSummary.updated_at >= datetime.now(UTC)-timedelta(hours=hours)) if hours: conditions.append(NewsSummary.updated_at >= datetime.now(UTC)-timedelta(hours=hours))
+3 -3
View File
@@ -1,7 +1,7 @@
"""Version-bound package relationships and evidence-backed event timelines.""" """Version-bound package relationships and evidence-backed event timelines."""
from datetime import UTC, datetime, timedelta from datetime import UTC, datetime, timedelta
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from sqlalchemy import select from sqlalchemy import select, func
from alphapulse.models.risk import NewsSummary, NewsEvent, EventMember, EventVersion, RiskSettings, EventLink from alphapulse.models.risk import NewsSummary, NewsEvent, EventMember, EventVersion, RiskSettings, EventLink
from alphapulse.core.db import SessionLocal from alphapulse.core.db import SessionLocal
from alphapulse.news_risk.pipeline import configuration from alphapulse.news_risk.pipeline import configuration
@@ -168,9 +168,9 @@ async def organize(target_id=None):
async def reading_inputs(session, cutoff): async def reading_inputs(session, cutoff):
packages = (await session.scalars(select(NewsSummary).where(NewsSummary.result.is_not(None), NewsSummary.replacement_ids == [], NewsSummary.state != 'withdrawn'))).all() packages = (await session.scalars(select(NewsSummary).where(func.jsonb_typeof(NewsSummary.result) == 'object', NewsSummary.replacement_ids == [], NewsSummary.state != 'withdrawn'))).all()
memberships = {m.package_id: m.event_id for m in (await session.scalars(select(EventMember))).all()} memberships = {m.package_id: m.event_id for m in (await session.scalars(select(EventMember))).all()}
events = {e.id: e for e in (await session.scalars(select(NewsEvent).where(NewsEvent.result.is_not(None), NewsEvent.state != 'retired'))).all()} events = {e.id: e for e in (await session.scalars(select(NewsEvent).where(func.jsonb_typeof(NewsEvent.result) == 'object', NewsEvent.state != 'retired'))).all()}
inputs, included = [], set() inputs, included = [], set()
for package in packages: for package in packages:
if package.updated_at < cutoff: continue if package.updated_at < cutoff: continue
+35 -1
View File
@@ -5,7 +5,9 @@ import os
from types import SimpleNamespace from types import SimpleNamespace
from unittest.mock import patch from unittest.mock import patch
from urllib.parse import urlparse from urllib.parse import urlparse
from sqlalchemy import select, func, delete import httpx
from fastapi import FastAPI
from sqlalchemy import select, func, delete, null
from alphapulse.core.db import SessionLocal, engine from alphapulse.core.db import SessionLocal, engine
from alphapulse.models import AiProvider, NewsItem, SkillNode, SkillVersion from alphapulse.models import AiProvider, NewsItem, SkillNode, SkillVersion
from alphapulse.models.risk import NewsSummary, ArticleAnalysis, SummaryArticle, RiskSettings, NewsEvent, EventMember, EventVersion, ReprocessJob, PackageCorrection, GroupingVersion, SummaryVersion, PackageEvidence, RiskSnapshot, EventLink from alphapulse.models.risk import NewsSummary, ArticleAnalysis, SummaryArticle, RiskSettings, NewsEvent, EventMember, EventVersion, ReprocessJob, PackageCorrection, GroupingVersion, SummaryVersion, PackageEvidence, RiskSnapshot, EventLink
@@ -50,6 +52,38 @@ async def main():
session.add(withdrawn); await session.flush() session.add(withdrawn); await session.flush()
session.add(EventMember(package_id=withdrawn.id,event_id=event_id,reason='Previous member')) session.add(EventMember(package_id=withdrawn.id,event_id=event_id,reason='Previous member'))
await session.commit() await session.commit()
async with SessionLocal() as session:
nested = await session.begin_nested()
await session.execute(NewsEvent.__table__.update().where(NewsEvent.id == event_id).values(result=None))
await session.execute(NewsSummary.__table__.update().where(NewsSummary.id.in_(package_ids)).values(state='pending'))
for empty in (None, null()):
session.add(NewsSummary(title='Unreviewed',fact='Pending',category='security',entities=[],
published_at=now,updated_at=now,changed_at=now,due_at=now,state='pending',result=empty))
session.add(NewsEvent(title='Unreviewed',updated_at=now,due_at=now,state='pending',result=empty))
for empty in (None, null(), {'inputs':None}):
session.add(RiskSnapshot(state='done',result=empty,quotes={},model='synthetic'))
await session.flush()
inputs = await events.reading_inputs(session,now-timedelta(hours=72))
assert len(briefing.semantic_packages(inputs)) == 2
assert {(p['kind'],p['id']) for p in inputs} == {('package',i) for i in package_ids}
from alphapulse.api import risk as api
app = FastAPI()
app.include_router(api.router)
app.dependency_overrides[api.require_admin] = lambda: None
app.dependency_overrides[api.get_session] = lambda: session
async with httpx.AsyncClient(transport=httpx.ASGITransport(app),base_url='http://ci') as client:
for hours in (24,48,72,168):
response = await client.get('/api/risk/overview',params={'hours':hours})
assert response.status_code == 200, response.text
data = response.json()['data']
assert data['briefing'] is None and data['stale']
assert sum(data['counts'].values()) == 2
response = await client.get('/api/risk/summaries')
assert response.status_code == 200, response.text
data = response.json()['data']
assert data['total'] == 2 and {p['id'] for p in data['items']} == set(package_ids)
await nested.rollback()
print('Reading API: SQL NULL and JSON null results excluded; pending events preserve independent packages')
async def complete(provider,system,payload,schema): async def complete(provider,system,payload,schema):
points=[] points=[]
for p in payload['packages']: for p in payload['packages']: