File size: 8,818 Bytes
db7ef8e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Offline deterministic agent — works without DeepSeek keys.



Pattern-matches the question to a SQL template + UI Plan. Always produces a

valid UIPlan with citations grounded in DuckDB results.

"""
from __future__ import annotations

import re
from typing import Any

from ..retrieval.duck import run_sql
from ..ui_plan.schema import (
    Citation,
    Component,
    Encoding,
    GridCell,
    Layout,
    Persona,
    UIPlan,
)

# ---- helpers ----------------------------------------------------------------

ACCENT = "#F47C2C"


def _kpi(id_: str, title: str, value: float | str, sub: str = "") -> Component:
    return Component(
        id=id_,
        type="kpi",
        title=title,
        subtitle=sub,
        data={"value": value, "format": "currency_inr" if isinstance(value, (int, float)) else "text"},
    )


def _table(id_: str, title: str, rows: list[dict[str, Any]]) -> Component:
    return Component(id=id_, type="data-table", title=title, data=rows[:200])


def _bar(id_: str, title: str, rows: list[dict[str, Any]],

         x: str, y: str) -> Component:
    return Component(
        id=id_,
        type="bar",
        title=title,
        data=rows,
        encoding=Encoding(x=x, y=y, color=ACCENT),
        why=f"Bar chart compares {y} across {x}.",
    )


def _line(id_: str, title: str, rows: list[dict[str, Any]],

          x: str, y: str) -> Component:
    return Component(
        id=id_,
        type="line",
        title=title,
        data=rows,
        encoding=Encoding(x=x, y=y, color=ACCENT),
        why=f"Line chart traces {y} over {x}.",
    )


def _wrap(question: str, narrative: str, components: list[Component],

          source_table: str, sql: str) -> UIPlan:
    grid: list[GridCell] = []
    y = 0
    for i, c in enumerate(components):
        if c.type == "kpi":
            grid.append(GridCell(id=f"g{i}", component=c.id, w=4, h=2, x=(i % 3) * 4, y=0))
        else:
            grid.append(GridCell(id=f"g{i}", component=c.id, w=12, h=5, x=0, y=y or 2))
            y += 5
    return UIPlan(
        persona=Persona(name="analyst", accent=ACCENT, mood="investigative"),
        layout=Layout(grid=grid),
        components=components,
        narrative=narrative,
        citations=[
            Citation(
                id="cite1",
                source=source_table,
                snippet=sql[:280],
            )
        ],
    )


# ---- intent matchers --------------------------------------------------------

def _try_top_funders(q: str) -> UIPlan | None:
    if not re.search(r"\b(top|biggest|largest)\b.*\bfunder", q, re.I):
        return None
    sql = """

        SELECT funder_name,

               ROUND(SUM(actual_total)::DOUBLE, 2) AS actual_inr

        FROM bva

        WHERE funder_name IS NOT NULL AND funder_name <> 'NONE'

        GROUP BY funder_name

        ORDER BY actual_inr DESC NULLS LAST

        LIMIT 10

    """
    res = run_sql(sql)
    if res.error or not res.rows:
        return None
    top = res.rows[0]
    grand = sum(float(r["actual_inr"] or 0) for r in res.rows)
    comps = [
        _kpi("k1", "Top funder",
             top["funder_name"], f"₹{top['actual_inr']:,.0f}"),
        _kpi("k2", "Top-10 funders total", grand, f"FY26 actual"),
        _kpi("k3", "Funders shown", len(res.rows), "rows"),
        _bar("c1", "Top 10 funders by FY26 actual spend",
             res.rows, x="funder_name", y="actual_inr"),
        _table("t1", "Detail", res.rows),
    ]
    narrative = (
        f"Across the top 10 funders, **{top['funder_name']}** leads at "
        f"₹{float(top['actual_inr']):,.0f}. Combined top-10 actual is "
        f"₹{grand:,.0f}, sourced from the `bva` ledger."
    )
    return _wrap(q, narrative, comps, "bva", sql)


def _try_monthly_burn(q: str) -> UIPlan | None:
    if not re.search(r"\b(monthly|month|trend|burn|over time)\b", q, re.I):
        return None
    cols = [f"actual_{m}" for m in [
        "01042025", "01052025", "01062025", "01072025", "01082025", "01092025",
        "01102025", "01112025", "01122025", "01012026", "01022026", "01032026"]]
    months = ["Apr-25", "May-25", "Jun-25", "Jul-25", "Aug-25", "Sep-25",
              "Oct-25", "Nov-25", "Dec-25", "Jan-26", "Feb-26", "Mar-26"]
    union = " UNION ALL ".join(
        f"SELECT '{m}' AS month, {i:02d} AS ord, "
        f"ROUND(SUM({c})::DOUBLE,2) AS actual_inr FROM bva"
        for i, (m, c) in enumerate(zip(months, cols), 1)
    )
    sql = f"SELECT * FROM ({union}) ORDER BY ord"
    res = run_sql(sql)
    if res.error or not res.rows:
        return None
    total = sum(float(r["actual_inr"] or 0) for r in res.rows)
    peak = max(res.rows, key=lambda r: float(r["actual_inr"] or 0))
    comps = [
        _kpi("k1", "FY26 actual (Apr–Mar)", total, "from bva monthly cols"),
        _kpi("k2", "Peak month", peak["month"], f"₹{peak['actual_inr']:,.0f}"),
        _kpi("k3", "Months", len(res.rows), "covered"),
        _line("c1", "Monthly burn — FY26",
              res.rows, x="month", y="actual_inr"),
        _table("t1", "Monthly detail", res.rows),
    ]
    narrative = (
        f"The FY26 monthly burn totals ₹{total:,.0f}, peaking in "
        f"**{peak['month']}** at ₹{float(peak['actual_inr']):,.0f}. "
        f"Trend is computed from the per-month `actual_*` columns of `bva`."
    )
    return _wrap(q, narrative, comps, "bva", sql)


def _try_top_categories(q: str) -> UIPlan | None:
    if not re.search(r"\bcategor", q, re.I):
        return None
    sql = """

        SELECT category_name,

               ROUND(SUM(actual_total)::DOUBLE, 2) AS actual_inr

        FROM bva

        WHERE category_name IS NOT NULL

        GROUP BY category_name

        ORDER BY actual_inr DESC NULLS LAST

        LIMIT 15

    """
    res = run_sql(sql)
    if res.error or not res.rows:
        return None
    comps = [
        _kpi("k1", "Top category", res.rows[0]["category_name"],
             f"₹{res.rows[0]['actual_inr']:,.0f}"),
        _bar("c1", "Top 15 spend categories", res.rows,
             x="category_name", y="actual_inr"),
        _table("t1", "Detail", res.rows),
    ]
    return _wrap(
        q,
        f"Top spending category is **{res.rows[0]['category_name']}** "
        f"at ₹{float(res.rows[0]['actual_inr']):,.0f}.",
        comps, "bva", sql)


def _try_open_po(q: str) -> UIPlan | None:
    if not re.search(r"\b(open\s*po|purchase order|po\b|vendor)\b", q, re.I):
        return None
    sql = "SELECT * FROM open_po LIMIT 200"
    res = run_sql(sql)
    if res.error or not res.rows:
        return None
    comps = [
        _kpi("k1", "Open POs", len(res.rows), "rows"),
        _table("t1", "Open Purchase Orders", res.rows),
    ]
    return _wrap(q, f"{len(res.rows)} open purchase orders found.",
                 comps, "open_po", sql)


def _try_overview(q: str) -> UIPlan:
    """Always-on default overview."""
    funders = run_sql("SELECT COUNT(DISTINCT funder_name) AS n FROM bva "
                      "WHERE funder_name IS NOT NULL AND funder_name <> 'NONE'")
    projects = run_sql("SELECT COUNT(DISTINCT project_id) AS n FROM bva")
    grand = run_sql("SELECT ROUND(SUM(actual_total)::DOUBLE,2) AS v FROM bva")
    region = run_sql("""

        SELECT COALESCE(region,'(unspecified)') AS region,

               ROUND(SUM(actual_total)::DOUBLE,2) AS actual_inr

        FROM bva

        GROUP BY region

        ORDER BY actual_inr DESC NULLS LAST

        LIMIT 12

    """)
    fcount = funders.rows[0]["n"] if funders.rows else 0
    pcount = projects.rows[0]["n"] if projects.rows else 0
    gv = grand.rows[0]["v"] if grand.rows else 0
    comps = [
        _kpi("k1", "Distinct funders", fcount, "in BVA ledger"),
        _kpi("k2", "Distinct projects", pcount, "tracked"),
        _kpi("k3", "FY26 actual (gross)", gv, "₹"),
        _bar("c1", "Spend by region", region.rows,
             x="region", y="actual_inr"),
        _table("t1", "Region breakdown", region.rows),
    ]
    return _wrap(q,
                 f"BVA spans **{fcount} funders** and **{pcount} projects** with "
                 f"a gross actual of ₹{float(gv):,.0f} for FY26.",
                 comps, "bva", region.sql)


MATCHERS = [
    _try_top_funders,
    _try_monthly_burn,
    _try_top_categories,
    _try_open_po,
]


def offline_plan(question: str) -> UIPlan:
    for fn in MATCHERS:
        try:
            plan = fn(question)
        except Exception:  # noqa: BLE001
            plan = None
        if plan is not None:
            return plan
    return _try_overview(question)