File size: 11,059 Bytes
de28957
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
from __future__ import annotations

import json
from typing import Any, Literal, Optional

from langchain_core.tools import tool
from psycopg.rows import dict_row

from app.db.postgres import get_readonly_connection

Metric = Literal[
    "fake_news_count",
    "articles_by_topic",
    "sentiment_breakdown",
    "propaganda_count",
    "hate_speech_count",
    "dialect_breakdown",
    "article_lookup",
]


async def _fetch(sql: str, params: list[Any]) -> list[dict[str, Any]]:
    async with get_readonly_connection() as conn:
        async with conn.cursor(row_factory=dict_row) as cur:
            await cur.execute(sql, params)
            return await cur.fetchall()


def _date_filter(where: list[str], params: list[Any], date_col: str,

                 date_from: Optional[str], date_to: Optional[str]) -> None:
    if date_from:
        where.append(f"{date_col} >= %s")
        params.append(date_from)
    if date_to:
        where.append(f"{date_col} <= %s")
        params.append(date_to)


async def _fake_news_count(date_from, date_to, article_id, limit):
    where: list[str] = []
    params: list[Any] = []
    _date_filter(where, params, "analyzed_at", date_from, date_to)
    if article_id is not None:
        where.append("article_id = %s")
        params.append(article_id)
    where_sql = f"WHERE {' AND '.join(where)}" if where else ""

    breakdown_sql = f"""

        SELECT verdict, COUNT(*) AS count

        FROM article_fake_news

        {where_sql}

        GROUP BY verdict

        ORDER BY count DESC

    """
    rows = await _fetch(breakdown_sql, params)

    refs_sql = f"""

        SELECT article_id

        FROM article_fake_news

        {where_sql}

        ORDER BY analyzed_at DESC

        LIMIT %s

    """
    refs = await _fetch(refs_sql, params + [limit])
    return breakdown_sql, rows, refs


async def _articles_by_topic(date_from, date_to, topic, article_id, limit):
    where: list[str] = []
    params: list[Any] = []
    _date_filter(where, params, "analyzed_at", date_from, date_to)
    if topic:
        where.append("(primary_topic = %s OR secondary_topic = %s)")
        params.extend([topic, topic])
    if article_id is not None:
        where.append("article_id = %s")
        params.append(article_id)
    where_sql = f"WHERE {' AND '.join(where)}" if where else ""

    breakdown_sql = f"""

        SELECT primary_topic, COUNT(*) AS count

        FROM article_topic

        {where_sql}

        GROUP BY primary_topic

        ORDER BY count DESC

    """
    rows = await _fetch(breakdown_sql, params)

    refs_sql = f"""

        SELECT article_id

        FROM article_topic

        {where_sql}

        ORDER BY analyzed_at DESC

        LIMIT %s

    """
    refs = await _fetch(refs_sql, params + [limit])
    return breakdown_sql, rows, refs


async def _sentiment_breakdown(date_from, date_to, sentiment, article_id, limit):
    where: list[str] = []
    params: list[Any] = []
    _date_filter(where, params, "analyzed_at", date_from, date_to)
    if sentiment:
        where.append("primary_sentiment = %s")
        params.append(sentiment)
    if article_id is not None:
        where.append("article_id = %s")
        params.append(article_id)
    where_sql = f"WHERE {' AND '.join(where)}" if where else ""

    breakdown_sql = f"""

        SELECT primary_sentiment, COUNT(*) AS count

        FROM article_sentiment

        {where_sql}

        GROUP BY primary_sentiment

        ORDER BY count DESC

    """
    rows = await _fetch(breakdown_sql, params)

    refs_sql = f"""

        SELECT article_id

        FROM article_sentiment

        {where_sql}

        ORDER BY analyzed_at DESC

        LIMIT %s

    """
    refs = await _fetch(refs_sql, params + [limit])
    return breakdown_sql, rows, refs


async def _propaganda_count(date_from, date_to, article_id, limit):
    where: list[str] = []
    params: list[Any] = []
    _date_filter(where, params, "analyzed_at", date_from, date_to)
    if article_id is not None:
        where.append("article_id = %s")
        params.append(article_id)
    where_sql = f"WHERE {' AND '.join(where)}" if where else ""
    propaganda_where = where + ["is_propaganda = true"]

    count_sql = f"""

        SELECT COUNT(*) AS count

        FROM article_propaganda

        WHERE {' AND '.join(propaganda_where)}

    """
    rows = await _fetch(count_sql, params)

    technique_sql = f"""

        SELECT technique, COUNT(*) AS count

        FROM article_propaganda, unnest(techniques) AS technique

        WHERE {' AND '.join(propaganda_where)}

        GROUP BY technique

        ORDER BY count DESC

        LIMIT 10

    """
    technique_rows = await _fetch(technique_sql, params)
    rows[0]["technique_breakdown"] = technique_rows

    refs_sql = f"""

        SELECT article_id

        FROM article_propaganda

        WHERE {' AND '.join(propaganda_where)}

        ORDER BY analyzed_at DESC

        LIMIT %s

    """
    refs = await _fetch(refs_sql, params + [limit])
    return count_sql, rows, refs


async def _hate_speech_count(date_from, date_to, category, article_id, limit):
    where: list[str] = []
    params: list[Any] = []
    _date_filter(where, params, "analyzed_at", date_from, date_to)
    if category:
        where.append("predicted_category = %s")
        params.append(category)
    if article_id is not None:
        where.append("article_id = %s")
        params.append(article_id)
    where_sql = f"WHERE {' AND '.join(where)}" if where else ""

    breakdown_sql = f"""

        SELECT predicted_category, COUNT(*) AS count

        FROM article_hate_speech

        {where_sql}

        GROUP BY predicted_category

        ORDER BY count DESC

    """
    rows = await _fetch(breakdown_sql, params)

    refs_sql = f"""

        SELECT article_id

        FROM article_hate_speech

        {where_sql}

        ORDER BY analyzed_at DESC

        LIMIT %s

    """
    refs = await _fetch(refs_sql, params + [limit])
    return breakdown_sql, rows, refs


async def _dialect_breakdown(date_from, date_to, dialect, article_id, limit):
    where: list[str] = []
    params: list[Any] = []
    _date_filter(where, params, "analyzed_at", date_from, date_to)
    if dialect:
        where.append("detected_dialect = %s")
        params.append(dialect)
    if article_id is not None:
        where.append("article_id = %s")
        params.append(article_id)
    where_sql = f"WHERE {' AND '.join(where)}" if where else ""

    breakdown_sql = f"""

        SELECT detected_dialect, COUNT(*) AS count

        FROM article_dialect

        {where_sql}

        GROUP BY detected_dialect

        ORDER BY count DESC

    """
    rows = await _fetch(breakdown_sql, params)

    refs_sql = f"""

        SELECT article_id

        FROM article_dialect

        {where_sql}

        ORDER BY analyzed_at DESC

        LIMIT %s

    """
    refs = await _fetch(refs_sql, params + [limit])
    return breakdown_sql, rows, refs


async def _article_lookup(date_from, date_to, article_id, keyword, limit):
    where: list[str] = []
    params: list[Any] = []
    _date_filter(where, params, "published_at", date_from, date_to)
    if article_id is not None:
        where.append("id = %s")
        params.append(article_id)
    if keyword:
        where.append("title ILIKE %s")
        params.append(f"%{keyword}%")
    where_sql = f"WHERE {' AND '.join(where)}" if where else ""

    sql = f"""

        SELECT id, title, url, published_at

        FROM news_articles

        {where_sql}

        ORDER BY published_at DESC

        LIMIT %s

    """
    rows = await _fetch(sql, params + [limit])
    refs = [{"article_id": r["id"]} for r in rows]
    return sql, rows, refs


@tool
async def sql_query_tool(

    metric: Metric,

    date_from: Optional[str] = None,

    date_to: Optional[str] = None,

    topic: Optional[str] = None,

    sentiment: Optional[str] = None,

    category: Optional[str] = None,

    dialect: Optional[str] = None,

    article_id: Optional[int] = None,

    keyword: Optional[str] = None,

    limit: int = 20,

) -> str:
    """

    Run a pre-defined, read-only query over analyzed news articles.



    metric: which statistic/lookup to run —

        - fake_news_count: breakdown of fake-news verdicts

          (SUPPORTED, REFUTED, PARTIALLY_TRUE, UNVERIFIABLE)

        - articles_by_topic: article counts grouped by primary topic

        - sentiment_breakdown: article counts grouped by sentiment

          (positive, negative, neutral)

        - propaganda_count: count of articles flagged as propaganda,

          plus a technique frequency breakdown

        - hate_speech_count: article counts grouped by hate-speech category

          (none, other, origin, gender, religion)

        - dialect_breakdown: article counts grouped by detected dialect

        - article_lookup: look up specific articles by id or title keyword



    date_from / date_to: ISO dates (YYYY-MM-DD). For all metrics except

        article_lookup, these filter on the analysis timestamp

        (`analyzed_at`) — i.e. when the ML model ran, not when the article

        was published. For article_lookup they filter on `published_at`.

    topic, sentiment, category, dialect: optional exact-match filters for

        the corresponding metric.

    article_id: restrict to a single article.

    keyword: for article_lookup, a substring to search for in the title.

    limit: max number of source article references / lookup rows to return.



    Returns a JSON string: {"rows": [...], "sql_used": "...",

    "source_refs": [{"article_id": ...}, ...]}

    """
    handlers = {
        "fake_news_count": lambda: _fake_news_count(date_from, date_to, article_id, limit),
        "articles_by_topic": lambda: _articles_by_topic(date_from, date_to, topic, article_id, limit),
        "sentiment_breakdown": lambda: _sentiment_breakdown(date_from, date_to, sentiment, article_id, limit),
        "propaganda_count": lambda: _propaganda_count(date_from, date_to, article_id, limit),
        "hate_speech_count": lambda: _hate_speech_count(date_from, date_to, category, article_id, limit),
        "dialect_breakdown": lambda: _dialect_breakdown(date_from, date_to, dialect, article_id, limit),
        "article_lookup": lambda: _article_lookup(date_from, date_to, article_id, keyword, limit),
    }

    handler = handlers.get(metric)
    if handler is None:
        return json.dumps({"rows": [], "sql_used": "", "source_refs": [],
                           "error": f"Unknown metric: {metric}"})

    sql_used, rows, refs = await handler()
    source_refs = [{"type": "article", "id": r["article_id"]} for r in refs]

    return json.dumps(
        {"rows": rows, "sql_used": sql_used.strip(), "source_refs": source_refs},
        default=str,
    )