Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions docs/reference/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,19 @@ type Agency {
label: String!
}

type AgencyPeriodMetrics {
period: String!
agencyKey: String!
agencyName: String
articleCount: Int!
avgSentimentScore: Float
pctPositive: Float
pctNegative: Float
avgReadabilityFlesch: Float
avgWordCount: Float
topThemes: [ThemeStats!]!
}

"""Agency statistics with article count"""
type AgencyStats {
name: String!
Expand Down Expand Up @@ -115,6 +128,14 @@ enum ArticleSort {
VIEWS
}

type ArticleSummary {
uniqueId: String!
title: String!
agencyName: String
publishedAt: String
trendingScore: Float
}

type ArticlesResult {
articles: [Article!]!
page: Int!
Expand Down Expand Up @@ -246,13 +267,31 @@ input DeliveryChannelsInput {
webhook: Boolean! = false
}

type EntityCoveragePoint {
period: String!
agencyKey: String!
agencyName: String
articleCount: Int!
totalMentions: Int!
avgSentimentScore: Float
}

type EntityFacet {
value: String!
count: Int!
entityId: String
label: String
}

enum EntityKind {
ORG
PER
LOC
EVENT
POLICY
LAW
}

type EntityNetwork {
nodes: [EntityNetworkNode!]!
edges: [EntityNetworkEdge!]!
Expand Down Expand Up @@ -283,6 +322,19 @@ type EntityNode {
agencyKey: String
}

type EntitySearchResult {
entityId: String!
canonicalName: String!
type: String!
description: String
wikidataUrl: String
agencyKey: String
aliases: [String!]!
articleCount: Int!
confidence: Float!
matchType: String!
}

type EntityType {
text: String!
type: String!
Expand Down Expand Up @@ -322,6 +374,12 @@ type FollowedListing {
followedAt: DateTime
}

enum Granularity {
DAY
WEEK
MONTH
}

type IntegrityCandidateType {
uniqueId: String!
url: String!
Expand Down Expand Up @@ -373,6 +431,13 @@ type MarketplaceRecorte {
keywords: [String!]!
}

enum MetricType {
VOLUME
SENTIMENT
READABILITY
THEMES
}

type Mutation {
"""Cria um novo clipping"""
createClipping(input: ClippingInput!): Clipping!
Expand Down Expand Up @@ -521,6 +586,12 @@ type Query {
"""Daily article counts for the given date range"""
articlesTimeline(range: DateRange!): [DailyCount!]!

"""Métricas de publicação por agência e período"""
agencyAnalytics(agencies: [String!]!, dateFrom: String!, dateTo: String!, granularity: Granularity! = MONTH, metrics: [MetricType!] = null): [AgencyPeriodMetrics!]!

"""Temas em crescimento comparando janela recente com baseline histórico"""
trendingThemes(windowDays: Int! = 7, baselineDays: Int! = 28, minArticles: Int! = 3, growthThreshold: Float! = 1.5, agencyKey: String = null, limit: Int! = 10): [TrendingThemeResult!]!

"""
Lista todos os clippings do usuario autenticado (autorados + inscritos)
"""
Expand Down Expand Up @@ -624,6 +695,15 @@ type Query {
Estima quantos artigos um recorte capturaria nas ultimas `sinceHours` horas. Replica `lib/estimate-recorte-count.ts`: filtro = themes OR-levels + agencies OR'd + published_at >= now-sinceHours; para keywords, conta por keyword (q em title,summary) e retorna o MAX; sem keywords, uma unica contagem. Substitui o mock `clippingEstimate`. PUBLICO.
"""
estimateRecorteCount(themes: [String!]!, agencies: [String!]!, keywords: [String!]!, sinceHours: Int! = 24): Int!

"""Série temporal de cobertura de uma entidade por agência"""
entityCoverage(entityId: String!, dateFrom: String = null, dateTo: String = null, granularity: Granularity! = MONTH): [EntityCoveragePoint!]!

"""Busca fuzzy de entidades por nome ou alias"""
entitySearch(query: String!, entityType: EntityKind = null, limit: Int! = 5): [EntitySearchResult!]!

"""Entidades NER com maior crescimento de cobertura (pré-computado)"""
trendingEntities(limit: Int! = 10): [TrendingEntityResult!]!
}

type Recorte {
Expand Down Expand Up @@ -716,6 +796,26 @@ type ThemeStats {
count: Int!
}

type TrendingEntityResult {
entityId: String!
canonicalName: String!
type: String!
trendingScore: Float!
volumeRatio: Float!
windowCount: Int!
windowAgencies: Int!
computedAt: String
}

type TrendingThemeResult {
themeLabel: String!
themeCode: String
windowCount: Int!
baselineDailyAvg: Float!
growthScore: Float!
topArticles: [ArticleSummary!]!
}

type TypesenseDocRecordType {
uniqueId: String!
title: String!
Expand Down
100 changes: 100 additions & 0 deletions docs/reference/schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,19 @@ type Agency {
label: String!
}

type AgencyPeriodMetrics {
period: String!
agencyKey: String!
agencyName: String
articleCount: Int!
avgSentimentScore: Float
pctPositive: Float
pctNegative: Float
avgReadabilityFlesch: Float
avgWordCount: Float
topThemes: [ThemeStats!]!
}

"""Agency statistics with article count"""
type AgencyStats {
name: String!
Expand Down Expand Up @@ -126,6 +139,14 @@ enum ArticleSort {
VIEWS
}

type ArticleSummary {
uniqueId: String!
title: String!
agencyName: String
publishedAt: String
trendingScore: Float
}

type ArticlesResult {
articles: [Article!]!
page: Int!
Expand Down Expand Up @@ -257,13 +278,31 @@ input DeliveryChannelsInput {
webhook: Boolean! = false
}

type EntityCoveragePoint {
period: String!
agencyKey: String!
agencyName: String
articleCount: Int!
totalMentions: Int!
avgSentimentScore: Float
}

type EntityFacet {
value: String!
count: Int!
entityId: String
label: String
}

enum EntityKind {
ORG
PER
LOC
EVENT
POLICY
LAW
}

type EntityNetwork {
nodes: [EntityNetworkNode!]!
edges: [EntityNetworkEdge!]!
Expand Down Expand Up @@ -294,6 +333,19 @@ type EntityNode {
agencyKey: String
}

type EntitySearchResult {
entityId: String!
canonicalName: String!
type: String!
description: String
wikidataUrl: String
agencyKey: String
aliases: [String!]!
articleCount: Int!
confidence: Float!
matchType: String!
}

type EntityType {
text: String!
type: String!
Expand Down Expand Up @@ -333,6 +385,12 @@ type FollowedListing {
followedAt: DateTime
}

enum Granularity {
DAY
WEEK
MONTH
}

type IntegrityCandidateType {
uniqueId: String!
url: String!
Expand Down Expand Up @@ -384,6 +442,13 @@ type MarketplaceRecorte {
keywords: [String!]!
}

enum MetricType {
VOLUME
SENTIMENT
READABILITY
THEMES
}

type Mutation {
"""Cria um novo clipping"""
createClipping(input: ClippingInput!): Clipping!
Expand Down Expand Up @@ -532,6 +597,12 @@ type Query {
"""Daily article counts for the given date range"""
articlesTimeline(range: DateRange!): [DailyCount!]!

"""Métricas de publicação por agência e período"""
agencyAnalytics(agencies: [String!]!, dateFrom: String!, dateTo: String!, granularity: Granularity! = MONTH, metrics: [MetricType!] = null): [AgencyPeriodMetrics!]!

"""Temas em crescimento comparando janela recente com baseline histórico"""
trendingThemes(windowDays: Int! = 7, baselineDays: Int! = 28, minArticles: Int! = 3, growthThreshold: Float! = 1.5, agencyKey: String = null, limit: Int! = 10): [TrendingThemeResult!]!

"""
Lista todos os clippings do usuario autenticado (autorados + inscritos)
"""
Expand Down Expand Up @@ -635,6 +706,15 @@ type Query {
Estima quantos artigos um recorte capturaria nas ultimas `sinceHours` horas. Replica `lib/estimate-recorte-count.ts`: filtro = themes OR-levels + agencies OR'd + published_at >= now-sinceHours; para keywords, conta por keyword (q em title,summary) e retorna o MAX; sem keywords, uma unica contagem. Substitui o mock `clippingEstimate`. PUBLICO.
"""
estimateRecorteCount(themes: [String!]!, agencies: [String!]!, keywords: [String!]!, sinceHours: Int! = 24): Int!

"""Série temporal de cobertura de uma entidade por agência"""
entityCoverage(entityId: String!, dateFrom: String = null, dateTo: String = null, granularity: Granularity! = MONTH): [EntityCoveragePoint!]!

"""Busca fuzzy de entidades por nome ou alias"""
entitySearch(query: String!, entityType: EntityKind = null, limit: Int! = 5): [EntitySearchResult!]!

"""Entidades NER com maior crescimento de cobertura (pré-computado)"""
trendingEntities(limit: Int! = 10): [TrendingEntityResult!]!
}

type Recorte {
Expand Down Expand Up @@ -727,6 +807,26 @@ type ThemeStats {
count: Int!
}

type TrendingEntityResult {
entityId: String!
canonicalName: String!
type: String!
trendingScore: Float!
volumeRatio: Float!
windowCount: Int!
windowAgencies: Int!
computedAt: String
}

type TrendingThemeResult {
themeLabel: String!
themeCode: String
windowCount: Int!
baselineDailyAvg: Float!
growthScore: Float!
topArticles: [ArticleSummary!]!
}

type TypesenseDocRecordType {
uniqueId: String!
title: String!
Expand Down
16 changes: 16 additions & 0 deletions src/graphql_api/datasources/postgres.py
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,16 @@ class TypesenseDocRecord(NewsRecord):
"""


_TRENDING_ENTITIES_SQL = """
SELECT entity_id, canonical_name, type,
trending_score, volume_ratio, window_count, window_agencies,
computed_at::text
FROM entity_trending_scores
ORDER BY trending_score DESC
LIMIT $1
"""


def _row_to_news_record(row: dict) -> NewsRecord:
tags = row.get("tags") or []
if isinstance(tags, str):
Expand Down Expand Up @@ -916,3 +926,9 @@ async def entity_search(
row["aliases"] = []
results.append(row)
return results

async def get_trending_entities(self, limit: int = 10) -> list[dict]:
"""Retorna entidades NER com maior trending score (pré-computado pelo DAG)."""
async with self._pool.acquire() as conn:
rows = await conn.fetch(_TRENDING_ENTITIES_SQL, limit)
return [dict(r) for r in rows]
Loading
Loading