taylerErbe commited on
Commit
28ac456
·
verified ·
1 Parent(s): 67b2349

Upload 8 files

Browse files
Files changed (9) hide show
  1. .gitattributes +2 -0
  2. README.md +26 -0
  3. app.py +1206 -0
  4. config.json +7 -0
  5. faiss_index.bin +3 -0
  6. gitattributes +35 -0
  7. gitattributes (2) +2 -0
  8. metadata.parquet +3 -0
  9. requirements.txt +6 -0
.gitattributes ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ faiss_index.bin filter=lfs diff=lfs merge=lfs -text
2
+ metadata.parquet filter=lfs diff=lfs merge=lfs -text
README.md ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Igpa Legislation Explorer
3
+ emoji: 🚀
4
+ colorFrom: red
5
+ colorTo: red
6
+ sdk: streamlit
7
+ app_file: app.py
8
+ app_port: 8501
9
+ tags:
10
+ - streamlit
11
+ - faiss
12
+ - semantic-search
13
+ pinned: false
14
+ short_description: IGPA semantic search and exploration of legislation
15
+ ---
16
+
17
+ # IGPA Legislation Explorer
18
+
19
+ This Streamlit app lets you perform semantic search over a corpus of legislation using a FAISS index and sentence-transformers embeddings. It supports:
20
+
21
+ - Free-text queries over bill summaries
22
+ - Filtering by intended beneficiaries, policy domain, and impact rating
23
+ - Viewing bill summaries, key provisions, and similarity scores
24
+ - Downloading search results as CSV for further analysis
25
+
26
+ To customize or extend the app, edit `app.py` at the root of this Space.
app.py ADDED
@@ -0,0 +1,1206 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import csv
4
+ from datetime import datetime
5
+
6
+ import numpy as np
7
+ import pandas as pd
8
+ import faiss
9
+ import streamlit as st
10
+ import altair as alt
11
+ from sentence_transformers import SentenceTransformer
12
+
13
+
14
+ # =========================================================
15
+ # PAGE CONFIG
16
+ # =========================================================
17
+ st.set_page_config(
18
+ page_title="IGPA Legislation Explorer",
19
+ layout="wide",
20
+ initial_sidebar_state="expanded"
21
+ )
22
+
23
+
24
+ # =========================================================
25
+ # PASSWORD GATE
26
+ # =========================================================
27
+ APP_PASSWORD = os.environ.get("APP_PASSWORD", "")
28
+
29
+ if "authed" not in st.session_state:
30
+ st.session_state.authed = False
31
+
32
+ if not st.session_state.authed:
33
+ st.markdown(
34
+ """
35
+ <style>
36
+ .login-title {
37
+ font-size: 2rem;
38
+ font-weight: 700;
39
+ text-align: center;
40
+ margin-top: 3rem;
41
+ margin-bottom: 0.35rem;
42
+ }
43
+ .login-subtitle {
44
+ text-align: center;
45
+ opacity: 0.8;
46
+ margin-bottom: 2rem;
47
+ }
48
+ </style>
49
+ """,
50
+ unsafe_allow_html=True,
51
+ )
52
+
53
+ st.markdown('<div class="login-title">IGPA Legislation Explorer</div>', unsafe_allow_html=True)
54
+ st.markdown(
55
+ '<div class="login-subtitle">Enter the application password to continue.</div>',
56
+ unsafe_allow_html=True,
57
+ )
58
+
59
+ _, center_col, _ = st.columns([1, 1.2, 1])
60
+
61
+ with center_col:
62
+ pw = st.text_input("Password", type="password")
63
+ if st.button("Enter", use_container_width=True):
64
+ if not APP_PASSWORD:
65
+ st.error("Server misconfigured: APP_PASSWORD not set in Space secrets.")
66
+ elif pw == APP_PASSWORD:
67
+ st.session_state.authed = True
68
+ st.rerun()
69
+ else:
70
+ st.error("Incorrect password.")
71
+
72
+ st.stop()
73
+
74
+
75
+ # =========================================================
76
+ # THEME-SAFE STYLES
77
+ # =========================================================
78
+ st.markdown(
79
+ """
80
+ <style>
81
+ .block-container {
82
+ padding-top: 3rem;
83
+ padding-bottom: 2rem;
84
+ padding-left: 1.5rem;
85
+ padding-right: 1.5rem;
86
+ }
87
+ .main-title {
88
+ font-size: 2rem;
89
+ font-weight: 700;
90
+ margin-bottom: 0.25rem;
91
+ }
92
+ .sub-title {
93
+ font-size: 1rem;
94
+ margin-bottom: 1rem;
95
+ opacity: 0.85;
96
+ }
97
+ .section-title {
98
+ font-size: 1.15rem;
99
+ font-weight: 700;
100
+ margin-top: 1rem;
101
+ margin-bottom: 0.7rem;
102
+ }
103
+ .kpi-card {
104
+ padding: 1rem 1rem 0.8rem 1rem;
105
+ border-radius: 14px;
106
+ border: 1px solid rgba(128,128,128,0.25);
107
+ background: rgba(255,255,255,0.04);
108
+ margin-bottom: 0.75rem;
109
+ }
110
+ .kpi-label {
111
+ font-size: 0.95rem;
112
+ opacity: 0.8;
113
+ margin-bottom: 0.35rem;
114
+ }
115
+ .kpi-value {
116
+ font-size: 2rem;
117
+ font-weight: 700;
118
+ line-height: 1.1;
119
+ }
120
+ .result-meta {
121
+ font-size: 0.9rem;
122
+ opacity: 0.8;
123
+ margin-top: 0.5rem;
124
+ margin-bottom: 0.25rem;
125
+ }
126
+ div[data-testid="stDataFrame"] {
127
+ border: 1px solid rgba(128,128,128,0.25);
128
+ border-radius: 12px;
129
+ overflow: hidden;
130
+ }
131
+ div.stButton > button {
132
+ border-radius: 10px;
133
+ font-weight: 600;
134
+ }
135
+ div[data-baseweb="select"] > div,
136
+ div[data-baseweb="popover"] > div,
137
+ div[data-testid="stDateInput"] > div,
138
+ div[data-baseweb="input"] > div {
139
+ border-radius: 10px;
140
+ }
141
+ div[data-baseweb="tag"] {
142
+ border-radius: 999px !important;
143
+ }
144
+ div[data-testid="metric-container"] svg {
145
+ display: none;
146
+ }
147
+ button[data-baseweb="tab"] {
148
+ color: #000 !important;
149
+ opacity: 1 !important;
150
+ font-weight: 600 !important;
151
+ }
152
+ button[data-baseweb="tab"][aria-selected="true"] {
153
+ border-bottom: 3px solid #2563eb !important;
154
+ }
155
+ </style>
156
+ """,
157
+ unsafe_allow_html=True
158
+ )
159
+
160
+
161
+ # =========================================================
162
+ # CONFIG
163
+ # =========================================================
164
+ DB_DIR = "."
165
+ FEEDBACK_CSV = os.path.join(DB_DIR, "impact_feedback.csv")
166
+ DEFAULT_TOP_K = 10
167
+
168
+ IMPACT_ORDER = [
169
+ "Not Impactful",
170
+ "Slightly Impactful",
171
+ "Moderately Impactful",
172
+ "Very Impactful"
173
+ ]
174
+
175
+ DATE_COL = "status_date"
176
+ SUMMARY_COL = "Llama Summary"
177
+ LINK_COL = "ftp_url"
178
+
179
+ CATEGORY_COL = "category_std"
180
+ SUBCATEGORY_COL = "subcategory_std"
181
+
182
+ POLICY_COL = "policy_domain_final"
183
+ BENEFICIARY_CATEGORY_COL = "intended_beneficiaries_category"
184
+ BENEFICIARY_SUBCATEGORY_COL = "intended_beneficiaries_subcategory"
185
+ BENEFICIARY_KEYWORD_COL = "intended_beneficiaries_keyword"
186
+ BENEFICIARY_COL = "intended_beneficiaries_final"
187
+
188
+ STRATEGY_COL = "legislative_strategy_std"
189
+ GOAL_COL = "legislative_goal_std"
190
+ INTENT_COL = "intent_std"
191
+ INCREASING_COL = "increasing_aspects_std"
192
+ DECREASING_COL = "decreasing_aspects_std"
193
+ MOTIVATION_COL = "motivation_std"
194
+
195
+ IMPACT_COL_PREFERRED = "impact_rating_std"
196
+ IMPACT_COL_FALLBACK = "Impact Rating"
197
+
198
+
199
+ # =========================================================
200
+ # SIDEBAR TOP ACTION
201
+ # =========================================================
202
+ with st.sidebar:
203
+ if st.button("Logout", use_container_width=True):
204
+ st.session_state.authed = False
205
+ st.rerun()
206
+
207
+
208
+ # =========================================================
209
+ # LOAD VECTOR DB
210
+ # =========================================================
211
+ @st.cache_resource
212
+ def load_vector_db(db_dir: str = DB_DIR):
213
+ with open(os.path.join(db_dir, "config.json"), "r", encoding="utf-8") as f:
214
+ cfg = json.load(f)
215
+
216
+ index = faiss.read_index(os.path.join(db_dir, "faiss_index.bin"))
217
+ meta = pd.read_parquet(os.path.join(db_dir, "metadata.parquet"))
218
+ meta = meta.reset_index(drop=True)
219
+
220
+ if "vec_id" not in meta.columns:
221
+ meta = meta.reset_index().rename(columns={"index": "vec_id"})
222
+
223
+ model = SentenceTransformer(cfg["embedding_model_name"])
224
+ return index, meta, model, cfg
225
+
226
+
227
+ index, meta_df, embed_model, cfg = load_vector_db()
228
+
229
+ IMPACT_COL = IMPACT_COL_PREFERRED if IMPACT_COL_PREFERRED in meta_df.columns else IMPACT_COL_FALLBACK
230
+
231
+ if DATE_COL in meta_df.columns:
232
+ meta_df[DATE_COL] = pd.to_datetime(meta_df[DATE_COL], errors="coerce")
233
+
234
+
235
+ # =========================================================
236
+ # HELPERS
237
+ # =========================================================
238
+ def impact_to_score(x):
239
+ if pd.isna(x):
240
+ return np.nan
241
+ x = str(x).strip().lower()
242
+ mapping = {
243
+ "not impactful": 0,
244
+ "slightly impactful": 1,
245
+ "moderately impactful": 2,
246
+ "very impactful": 3
247
+ }
248
+ return mapping.get(x, np.nan)
249
+
250
+
251
+ if "impact_rating_score" not in meta_df.columns and IMPACT_COL in meta_df.columns:
252
+ meta_df["impact_rating_score"] = meta_df[IMPACT_COL].apply(impact_to_score)
253
+
254
+
255
+ DEFAULT_FILTERS = {
256
+ "categories": [],
257
+ "subcategories": [],
258
+ "beneficiary_categories": [],
259
+ "beneficiary_subcategories": [],
260
+ "policy_domains": [],
261
+ "impact_selected": [],
262
+ "bill_statuses": [],
263
+ "date_range": (
264
+ meta_df[DATE_COL].min().date()
265
+ if DATE_COL in meta_df.columns and pd.notna(meta_df[DATE_COL].min())
266
+ else datetime.utcnow().date(),
267
+ meta_df[DATE_COL].max().date()
268
+ if DATE_COL in meta_df.columns and pd.notna(meta_df[DATE_COL].max())
269
+ else datetime.utcnow().date(),
270
+ ),
271
+ "num_search_results": DEFAULT_TOP_K,
272
+ }
273
+
274
+ for key, value in DEFAULT_FILTERS.items():
275
+ if key not in st.session_state:
276
+ st.session_state[key] = value
277
+
278
+ if "search_results" not in st.session_state:
279
+ st.session_state.search_results = None
280
+ if "current_query" not in st.session_state:
281
+ st.session_state.current_query = ""
282
+ if "history" not in st.session_state:
283
+ st.session_state.history = []
284
+
285
+
286
+ def embed_query(query: str):
287
+ return embed_model.encode(
288
+ [query],
289
+ normalize_embeddings=True,
290
+ convert_to_numpy=True
291
+ ).astype("float32")
292
+
293
+
294
+ def append_feedback_row(
295
+ bill_id,
296
+ predicted_impact,
297
+ user_response,
298
+ corrected_impact=None,
299
+ comment=None,
300
+ path=FEEDBACK_CSV,
301
+ ):
302
+ try:
303
+ file_exists = os.path.isfile(path)
304
+ with open(path, "a", newline="", encoding="utf-8") as f:
305
+ writer = csv.writer(f)
306
+ if not file_exists:
307
+ writer.writerow(
308
+ [
309
+ "timestamp",
310
+ "bill_id",
311
+ "predicted_impact",
312
+ "user_response",
313
+ "corrected_impact",
314
+ "comment",
315
+ ]
316
+ )
317
+ writer.writerow(
318
+ [
319
+ datetime.utcnow().isoformat(),
320
+ bill_id,
321
+ predicted_impact,
322
+ user_response,
323
+ corrected_impact if corrected_impact else "",
324
+ comment if comment else "",
325
+ ]
326
+ )
327
+ except Exception as e:
328
+ st.error(f"Failed to save feedback: {str(e)}")
329
+
330
+
331
+ def get_multiselect_options(df, col_name):
332
+ if col_name not in df.columns:
333
+ return []
334
+ vals = df[col_name].dropna().astype(str).str.strip()
335
+ return sorted([v for v in vals.unique().tolist() if v])
336
+
337
+
338
+ def get_dependent_subcategories(df, category_col, subcategory_col, selected_categories):
339
+ if subcategory_col not in df.columns:
340
+ return []
341
+
342
+ sub_df = df.copy()
343
+ if selected_categories and category_col in df.columns:
344
+ sub_df = sub_df[sub_df[category_col].isin(selected_categories)]
345
+
346
+ vals = sub_df[subcategory_col].dropna().astype(str).str.strip()
347
+ return sorted([v for v in vals.unique().tolist() if v])
348
+
349
+
350
+ def get_beneficiary_categories(df):
351
+ if BENEFICIARY_CATEGORY_COL not in df.columns:
352
+ return []
353
+ vals = df[BENEFICIARY_CATEGORY_COL].dropna().astype(str).str.strip()
354
+ return sorted([v for v in vals.unique().tolist() if v])
355
+
356
+
357
+ def get_beneficiary_subcategories(df, selected_categories):
358
+ return get_dependent_subcategories(
359
+ df,
360
+ BENEFICIARY_CATEGORY_COL,
361
+ BENEFICIARY_SUBCATEGORY_COL,
362
+ selected_categories
363
+ )
364
+
365
+
366
+ def get_categories(df):
367
+ if CATEGORY_COL not in df.columns:
368
+ return []
369
+ vals = df[CATEGORY_COL].dropna().astype(str).str.strip()
370
+ return sorted([v for v in vals.unique().tolist() if v])
371
+
372
+
373
+ def get_subcategories(df, selected_categories):
374
+ return get_dependent_subcategories(
375
+ df,
376
+ CATEGORY_COL,
377
+ SUBCATEGORY_COL,
378
+ selected_categories
379
+ )
380
+
381
+
382
+ def build_filter_mask(df):
383
+ mask = pd.Series(True, index=df.index)
384
+
385
+ selected_categories = st.session_state.get("categories", [])
386
+ if selected_categories and CATEGORY_COL in df.columns:
387
+ mask &= df[CATEGORY_COL].isin(selected_categories)
388
+
389
+ selected_subcategories = st.session_state.get("subcategories", [])
390
+ if selected_subcategories and SUBCATEGORY_COL in df.columns:
391
+ mask &= df[SUBCATEGORY_COL].isin(selected_subcategories)
392
+
393
+ selected_beneficiary_categories = st.session_state.get("beneficiary_categories", [])
394
+ if selected_beneficiary_categories and BENEFICIARY_CATEGORY_COL in df.columns:
395
+ mask &= df[BENEFICIARY_CATEGORY_COL].isin(selected_beneficiary_categories)
396
+
397
+ selected_beneficiary_subcategories = st.session_state.get("beneficiary_subcategories", [])
398
+ if selected_beneficiary_subcategories and BENEFICIARY_SUBCATEGORY_COL in df.columns:
399
+ mask &= df[BENEFICIARY_SUBCATEGORY_COL].isin(selected_beneficiary_subcategories)
400
+
401
+ selected_policy_domains = st.session_state.get("policy_domains", [])
402
+ if selected_policy_domains and POLICY_COL in df.columns:
403
+ mask &= df[POLICY_COL].isin(selected_policy_domains)
404
+
405
+ selected_impact = st.session_state.get("impact_selected", [])
406
+ if selected_impact and IMPACT_COL in df.columns:
407
+ mask &= df[IMPACT_COL].isin(selected_impact)
408
+
409
+ selected_statuses = st.session_state.get("bill_statuses", [])
410
+ if selected_statuses and "status_desc" in df.columns:
411
+ mask &= df["status_desc"].isin(selected_statuses)
412
+
413
+ if "date_range" in st.session_state and st.session_state.date_range and DATE_COL in df.columns:
414
+ dr = st.session_state.date_range
415
+ if isinstance(dr, (tuple, list)) and len(dr) == 2:
416
+ start, end = dr
417
+ else:
418
+ start = end = dr
419
+
420
+ start = pd.to_datetime(start)
421
+ end = pd.to_datetime(end)
422
+ mask &= df[DATE_COL].between(start, end)
423
+
424
+ return mask
425
+
426
+
427
+ def reset_filters():
428
+ for key, value in DEFAULT_FILTERS.items():
429
+ st.session_state[key] = value
430
+ st.session_state.search_results = None
431
+ st.session_state.current_query = ""
432
+ st.rerun()
433
+
434
+
435
+ def get_first_available(row, cols, default=""):
436
+ for c in cols:
437
+ if c in row.index:
438
+ val = row.get(c)
439
+ if pd.notna(val) and str(val).strip():
440
+ return val
441
+ return default
442
+
443
+
444
+ # =========================================================
445
+ # SIDEBAR FILTERS
446
+ # =========================================================
447
+ with st.sidebar:
448
+ st.header("Filters")
449
+
450
+ if st.button("Reset Filters", use_container_width=True):
451
+ reset_filters()
452
+
453
+ st.caption("Tip: Apply filters here before reviewing bills and charts.")
454
+
455
+ st.markdown("### Category & Subcategory")
456
+
457
+ has_category_cols = CATEGORY_COL in meta_df.columns and SUBCATEGORY_COL in meta_df.columns
458
+
459
+ if has_category_cols:
460
+ category_options = get_categories(meta_df)
461
+ st.multiselect(
462
+ "Category",
463
+ options=category_options,
464
+ key="categories"
465
+ )
466
+
467
+ subcategory_options = get_subcategories(
468
+ meta_df,
469
+ st.session_state.categories
470
+ )
471
+
472
+ current_subcats = st.session_state.get("subcategories", [])
473
+ valid_current_subcats = [x for x in current_subcats if x in subcategory_options]
474
+ if current_subcats != valid_current_subcats:
475
+ st.session_state["subcategories"] = valid_current_subcats
476
+
477
+ st.multiselect(
478
+ "Subcategory",
479
+ options=subcategory_options,
480
+ key="subcategories"
481
+ )
482
+ else:
483
+ st.info("Category and Subcategory filters are unavailable because 'category_std' and 'subcategory_std' are not present in the dataset.")
484
+
485
+ st.markdown("### Intended Beneficiaries")
486
+
487
+ beneficiary_category_options = get_beneficiary_categories(meta_df)
488
+ st.multiselect(
489
+ "Intended Beneficiary Category",
490
+ options=beneficiary_category_options,
491
+ key="beneficiary_categories"
492
+ )
493
+
494
+ beneficiary_subcategory_options = get_beneficiary_subcategories(
495
+ meta_df,
496
+ st.session_state.beneficiary_categories
497
+ )
498
+
499
+ current_beneficiary_subcats = st.session_state.get("beneficiary_subcategories", [])
500
+ valid_beneficiary_subcats = [x for x in current_beneficiary_subcats if x in beneficiary_subcategory_options]
501
+ if current_beneficiary_subcats != valid_beneficiary_subcats:
502
+ st.session_state["beneficiary_subcategories"] = valid_beneficiary_subcats
503
+
504
+ st.multiselect(
505
+ "Intended Beneficiary Subcategory",
506
+ options=beneficiary_subcategory_options,
507
+ key="beneficiary_subcategories"
508
+ )
509
+
510
+ st.markdown("### Policy, Impact, Status")
511
+
512
+ st.multiselect(
513
+ "Policy Area",
514
+ options=get_multiselect_options(meta_df, POLICY_COL),
515
+ key="policy_domains"
516
+ )
517
+
518
+ st.multiselect(
519
+ "Impact Rating (STD)",
520
+ options=IMPACT_ORDER,
521
+ key="impact_selected"
522
+ )
523
+
524
+ st.multiselect(
525
+ "Bill Status",
526
+ options=get_multiselect_options(meta_df, "status_desc"),
527
+ key="bill_statuses"
528
+ )
529
+
530
+ st.markdown("### Time Filter")
531
+
532
+ min_date = (
533
+ meta_df[DATE_COL].min().date()
534
+ if DATE_COL in meta_df.columns and pd.notna(meta_df[DATE_COL].min())
535
+ else datetime.utcnow().date()
536
+ )
537
+ max_date = (
538
+ meta_df[DATE_COL].max().date()
539
+ if DATE_COL in meta_df.columns and pd.notna(meta_df[DATE_COL].max())
540
+ else datetime.utcnow().date()
541
+ )
542
+
543
+ st.date_input(
544
+ "Status Date Range",
545
+ value=st.session_state.get("date_range", (min_date, max_date)),
546
+ min_value=min_date,
547
+ max_value=max_date,
548
+ key="date_range"
549
+ )
550
+
551
+ if os.path.exists(FEEDBACK_CSV):
552
+ try:
553
+ df_feedback = pd.read_csv(FEEDBACK_CSV)
554
+ st.info(f"Feedback records: {len(df_feedback)}")
555
+ with open(FEEDBACK_CSV, "rb") as f:
556
+ st.download_button(
557
+ label="Download impact_feedback.csv",
558
+ data=f.read(),
559
+ file_name="impact_feedback.csv",
560
+ mime="text/csv"
561
+ )
562
+ except Exception:
563
+ st.info("Feedback CSV ready")
564
+
565
+ with st.expander("Search History"):
566
+ for i, item in enumerate(reversed(st.session_state.history[-5:]), 1):
567
+ st.write(f"{i}. {item.get('query', '')}")
568
+
569
+
570
+ # =========================================================
571
+ # FILTERED DATA
572
+ # =========================================================
573
+ filtered_df = meta_df[build_filter_mask(meta_df)].copy()
574
+
575
+
576
+ # =========================================================
577
+ # TABS
578
+ # =========================================================
579
+ tab_search, tab_trends = st.tabs(["Search & Results", "Trends & Insights"])
580
+
581
+
582
+ # =========================================================
583
+ # SEARCH TAB
584
+ # =========================================================
585
+ with tab_search:
586
+ st.markdown('<div class="main-title">IGPA Legislation Explorer</div>', unsafe_allow_html=True)
587
+ st.markdown(
588
+ '<div class="sub-title">Search, filter, and analyze legislative bills by impact, policy area, category, subcategory, and intended beneficiaries.</div>',
589
+ unsafe_allow_html=True
590
+ )
591
+
592
+ col1, col2, col3, col4 = st.columns(4)
593
+
594
+ total_bills = len(filtered_df)
595
+ policy_count = filtered_df[POLICY_COL].nunique() if POLICY_COL in filtered_df.columns else 0
596
+ beneficiary_count = filtered_df[BENEFICIARY_COL].nunique() if BENEFICIARY_COL in filtered_df.columns else 0
597
+ very_impactful = (filtered_df[IMPACT_COL] == "Very Impactful").sum() if IMPACT_COL in filtered_df.columns else 0
598
+
599
+ with col1:
600
+ st.markdown(f"""
601
+ <div class="kpi-card">
602
+ <div class="kpi-label">Total Bills</div>
603
+ <div class="kpi-value">{total_bills}</div>
604
+ </div>
605
+ """, unsafe_allow_html=True)
606
+
607
+ with col2:
608
+ st.markdown(f"""
609
+ <div class="kpi-card">
610
+ <div class="kpi-label">Policy Domains</div>
611
+ <div class="kpi-value">{policy_count}</div>
612
+ </div>
613
+ """, unsafe_allow_html=True)
614
+
615
+ with col3:
616
+ st.markdown(f"""
617
+ <div class="kpi-card">
618
+ <div class="kpi-label">Beneficiary Groups</div>
619
+ <div class="kpi-value">{beneficiary_count}</div>
620
+ </div>
621
+ """, unsafe_allow_html=True)
622
+
623
+ with col4:
624
+ st.markdown(f"""
625
+ <div class="kpi-card">
626
+ <div class="kpi-label">Very Impactful Bills</div>
627
+ <div class="kpi-value">{very_impactful}</div>
628
+ </div>
629
+ """, unsafe_allow_html=True)
630
+
631
+ st.markdown('<div class="section-title">Most Impacted Beneficiary Keywords</div>', unsafe_allow_html=True)
632
+
633
+ if BENEFICIARY_KEYWORD_COL in filtered_df.columns and "impact_rating_score" in filtered_df.columns:
634
+ impact_df = (
635
+ filtered_df.dropna(subset=[BENEFICIARY_KEYWORD_COL, "impact_rating_score"])
636
+ .groupby(BENEFICIARY_KEYWORD_COL)
637
+ .agg(
638
+ avg_impact=("impact_rating_score", "mean"),
639
+ bills=("bill_id", "count"),
640
+ top_bills=("title", lambda x: "; ".join(x.head(5)))
641
+ )
642
+ .reset_index()
643
+ .sort_values(["avg_impact", "bills"], ascending=[False, False])
644
+ .head(15)
645
+ )
646
+
647
+ if not impact_df.empty:
648
+ chart = (
649
+ alt.Chart(impact_df)
650
+ .mark_bar()
651
+ .encode(
652
+ x=alt.X(f"{BENEFICIARY_KEYWORD_COL}:N", sort="-y", title="Beneficiary Keyword"),
653
+ y=alt.Y("avg_impact:Q", title="Average Impact Score"),
654
+ color=alt.Color(
655
+ "avg_impact:Q",
656
+ scale=alt.Scale(domain=[0, 3], range=["#fde68a", "#dc2626"]),
657
+ legend=alt.Legend(title="Average Impact")
658
+ ),
659
+ tooltip=[
660
+ alt.Tooltip(f"{BENEFICIARY_KEYWORD_COL}:N", title="Beneficiary Keyword"),
661
+ alt.Tooltip("avg_impact:Q", format=".2f", title="Average Impact"),
662
+ alt.Tooltip("bills:Q", title="Number of Bills"),
663
+ alt.Tooltip("top_bills:N", title="Top Bills")
664
+ ]
665
+ )
666
+ .properties(height=350)
667
+ )
668
+ st.altair_chart(chart, use_container_width=True)
669
+ else:
670
+ st.info("No beneficiary keyword impact data available for the current filters.")
671
+ else:
672
+ st.info("Beneficiary keyword or impact score columns are not available.")
673
+
674
+ table_header_col1 = st.columns([1])[0]
675
+
676
+ with table_header_col1:
677
+ st.markdown('<div class="section-title">Bills Matching Selected Filters</div>', unsafe_allow_html=True)
678
+
679
+ st.caption(
680
+ "These rows reflect the active toolbar filters. ILGA columns are source legislative fields. "
681
+ "Llama columns are AI-generated summaries or standardized outputs."
682
+ )
683
+
684
+ display_cols = {
685
+ "bill_number": "Bill Number (ILGA)",
686
+ "title": "Title (ILGA)",
687
+ "description": "Description (ILGA)",
688
+ CATEGORY_COL: "Category",
689
+ SUBCATEGORY_COL: "Subcategory",
690
+ BENEFICIARY_CATEGORY_COL: "Intended Beneficiary Category",
691
+ BENEFICIARY_SUBCATEGORY_COL: "Intended Beneficiary Subcategory",
692
+ SUMMARY_COL: "Llama Summary",
693
+ "Potential Impact": "Potential Impact (Llama)",
694
+ IMPACT_COL: "Impact Rating (STD)",
695
+ "status_desc": "Status (ILGA)",
696
+ LINK_COL: "Bill Link (ILGA)"
697
+ }
698
+
699
+ available_cols = [c for c in display_cols if c in filtered_df.columns]
700
+
701
+ filter_bill_df = (
702
+ filtered_df[available_cols]
703
+ .rename(columns=display_cols)
704
+ .copy()
705
+ )
706
+
707
+ st.dataframe(
708
+ filter_bill_df,
709
+ use_container_width=True,
710
+ column_config={
711
+ "Bill Link (ILGA)": st.column_config.LinkColumn(
712
+ label="Bill Link (ILGA)",
713
+ display_text="Open Bill"
714
+ )
715
+ } if "Bill Link (ILGA)" in filter_bill_df.columns else None
716
+ )
717
+
718
+ csv = filter_bill_df.to_csv(index=False).encode("utf-8")
719
+
720
+ st.download_button(
721
+ label="Download as CSV",
722
+ data=csv,
723
+ file_name="filtered_bills.csv",
724
+ mime="text/csv"
725
+ )
726
+
727
+ search_header_col1, search_header_col2 = st.columns([6, 2])
728
+
729
+ with search_header_col1:
730
+ st.markdown('<div class="section-title">Search Bills</div>', unsafe_allow_html=True)
731
+
732
+ with search_header_col2:
733
+ st.slider(
734
+ "Number of Results – Search Query Results",
735
+ 5, 50, st.session_state.get("num_search_results", DEFAULT_TOP_K), 5,
736
+ key="num_search_results"
737
+ )
738
+
739
+ search_col, button_col = st.columns([6, 1])
740
+
741
+ with search_col:
742
+ query = st.text_input(
743
+ "Ask a question about legislation",
744
+ value=st.session_state.current_query,
745
+ placeholder="Example: bills related to higher education funding, healthcare workers, or workforce policy"
746
+ )
747
+
748
+ with button_col:
749
+ st.markdown("<div style='height: 28px;'></div>", unsafe_allow_html=True)
750
+ search_clicked = st.button("Search", use_container_width=True)
751
+
752
+ if search_clicked and query.strip():
753
+ st.session_state.current_query = query
754
+ st.session_state.history.append({"query": query})
755
+
756
+ q_vec = embed_query(query)
757
+ top_k = st.session_state.num_search_results
758
+ n_search = min(len(meta_df), top_k * 5)
759
+
760
+ scores, ids = index.search(q_vec, n_search)
761
+ ids, scores = ids[0], scores[0]
762
+
763
+ allowed = set(filtered_df.index)
764
+ kept = [(i, s) for i, s in zip(ids, scores) if i in allowed][:top_k]
765
+
766
+ if not kept:
767
+ st.warning("No results found for this query under the current filters.")
768
+ st.session_state.search_results = None
769
+ else:
770
+ results = meta_df.loc[[i for i, _ in kept]].copy()
771
+ results["similarity"] = [s for _, s in kept]
772
+ st.session_state.search_results = results
773
+
774
+ if st.session_state.search_results is not None:
775
+ results = st.session_state.search_results
776
+
777
+ st.markdown('<div class="section-title">Filtered Results Table</div>', unsafe_allow_html=True)
778
+ st.caption("Search results are ranked by semantic similarity after applying the active toolbar filters.")
779
+ st.caption("ILGA fields come from source legislation data. Llama fields are AI-generated summaries or standardized outputs.")
780
+
781
+ review_cols = [
782
+ "bill_number",
783
+ "title",
784
+ "description",
785
+ CATEGORY_COL,
786
+ SUBCATEGORY_COL,
787
+ BENEFICIARY_CATEGORY_COL,
788
+ BENEFICIARY_SUBCATEGORY_COL,
789
+ SUMMARY_COL,
790
+ GOAL_COL,
791
+ "Potential Impact",
792
+ INCREASING_COL,
793
+ DECREASING_COL,
794
+ IMPACT_COL,
795
+ "similarity",
796
+ LINK_COL
797
+ ]
798
+
799
+ review_df = results[[c for c in review_cols if c in results.columns]].copy()
800
+
801
+ review_df.rename(
802
+ columns={
803
+ "bill_number": "Bill Number",
804
+ "title": "Title",
805
+ "description": "Description from Legiscan",
806
+ CATEGORY_COL: "Category",
807
+ SUBCATEGORY_COL: "Subcategory",
808
+ BENEFICIARY_CATEGORY_COL: "Main Category (LLaMA Generated Response)",
809
+ BENEFICIARY_SUBCATEGORY_COL: "Beneficiary Subcategory",
810
+ SUMMARY_COL: "LLaMA Summary",
811
+ GOAL_COL: "Legislative Goal",
812
+ "Potential Impact": "Potential Impact (LLaMA)",
813
+ INCREASING_COL: "Increasing Aspects (STD)",
814
+ DECREASING_COL: "Decreasing Aspects (STD)",
815
+ IMPACT_COL: "Impact Rating (STD)",
816
+ "similarity": "Similarity Score",
817
+ LINK_COL: "Bill URL (ILGA)"
818
+ },
819
+ inplace=True
820
+ )
821
+
822
+ st.dataframe(
823
+ review_df,
824
+ use_container_width=True,
825
+ column_config={
826
+ "Bill URL (ILGA)": st.column_config.LinkColumn(
827
+ "Bill URL (ILGA)",
828
+ display_text="Open bill"
829
+ )
830
+ } if "Bill URL (ILGA)" in review_df.columns else None
831
+ )
832
+
833
+ csv_data = review_df.to_csv(index=False).encode("utf-8")
834
+ st.download_button(
835
+ label="Download CSV",
836
+ data=csv_data,
837
+ file_name="search_results.csv",
838
+ mime="text/csv"
839
+ )
840
+
841
+ st.markdown("---")
842
+ st.markdown('<div class="section-title">Top Matching Bills</div>', unsafe_allow_html=True)
843
+
844
+ for idx, row in results.iterrows():
845
+ bill_number_val = get_first_available(row, ["bill_number"])
846
+ title_val = get_first_available(row, ["title"])
847
+ description_val = get_first_available(row, ["description"])
848
+ category_val = get_first_available(row, [CATEGORY_COL])
849
+ subcategory_val = get_first_available(row, [SUBCATEGORY_COL])
850
+ summary_val = get_first_available(row, [SUMMARY_COL])
851
+
852
+ st.markdown(f"**Bill Number** \n{bill_number_val if bill_number_val else 'N/A'}")
853
+ st.markdown(f"**Title** \n{title_val if title_val else 'N/A'}")
854
+
855
+ if description_val:
856
+ st.markdown(f"**Description from Legiscan** \n{description_val}")
857
+
858
+ if category_val:
859
+ st.markdown(f"**Category** \n{category_val}")
860
+
861
+ if subcategory_val:
862
+ st.markdown(f"**Subcategory** \n{subcategory_val}")
863
+
864
+ if pd.notna(row.get(LINK_COL)):
865
+ st.markdown(f"[Open Full Bill]({row.get(LINK_COL)})")
866
+
867
+ if summary_val:
868
+ with st.expander("Summary from LLaMA", expanded=True):
869
+ st.write(summary_val)
870
+
871
+ detail_map = {
872
+ "Status": "status_desc",
873
+ "Legislative Goal": "Legislative Goal",
874
+ "Key Provisions": "Key Provisions",
875
+ "Increasing Aspects": "Increasing Aspects",
876
+ "Decreasing Aspects": "Decreasing Aspects",
877
+ "Category & Subcategory": "Category & Subcategory",
878
+ "Ideological Alignment": "Ideological Alignment",
879
+ "Potential Impact": "Potential Impact",
880
+ "Original Law": "Original Law",
881
+ "committee": "committee",
882
+ "last_action_date": "last_action_date",
883
+ "last_action": "last_action",
884
+ "ILGA State Link": "state_link",
885
+ "Legiscan URL": "url",
886
+ "ftp_url": "ftp_url",
887
+ }
888
+
889
+ with st.expander("More Details"):
890
+ for label, col_name in detail_map.items():
891
+ if col_name in results.columns:
892
+ val = row.get(col_name)
893
+ if pd.notna(val) and str(val).strip():
894
+ if label in ["ILGA State Link", "Legiscan URL", "ftp_url"]:
895
+ st.markdown(f"**{label}:** [Open Link]({val})")
896
+ else:
897
+ st.write(f"**{label}:** {val}")
898
+
899
+ with st.expander("Impact Rating Accuracy", expanded=False):
900
+ st.markdown("**Is this impact rating accurate?**")
901
+ predicted_impact = row.get(IMPACT_COL, "")
902
+ bill_id_safe = str(row.get("bill_id", idx))
903
+
904
+ feedback_submitted = st.session_state.get(f"feedback_done_{bill_id_safe}", False)
905
+
906
+ if feedback_submitted:
907
+ st.success("Thank you for your feedback.")
908
+ st.caption(f"Bill: {row.get('bill_number', 'N/A')} | Saved to impact_feedback.csv")
909
+ else:
910
+ col_yes, col_no = st.columns(2)
911
+
912
+ with col_yes:
913
+ if st.button("Yes - Accurate", key=f"yes_{bill_id_safe}", use_container_width=True):
914
+ append_feedback_row(
915
+ bill_id=bill_id_safe,
916
+ predicted_impact=predicted_impact,
917
+ user_response="Yes",
918
+ corrected_impact=None,
919
+ comment=None,
920
+ )
921
+ st.session_state[f"feedback_done_{bill_id_safe}"] = True
922
+ st.rerun()
923
+
924
+ with col_no:
925
+ if st.button("No - Incorrect", key=f"no_{bill_id_safe}", use_container_width=True):
926
+ st.session_state[f"show_corrected_{bill_id_safe}"] = True
927
+ st.rerun()
928
+
929
+ if st.session_state.get(f"show_corrected_{bill_id_safe}", False):
930
+ corrected_value = st.selectbox(
931
+ "Correct impact rating",
932
+ IMPACT_ORDER,
933
+ key=f"corrected_{bill_id_safe}",
934
+ )
935
+
936
+ comment = st.text_area(
937
+ "Optional correction note",
938
+ max_chars=250,
939
+ key=f"comment_{bill_id_safe}",
940
+ placeholder="Add a short note explaining why the impact rating should change"
941
+ )
942
+
943
+ col_submit, col_cancel = st.columns([3, 1])
944
+
945
+ with col_submit:
946
+ if st.button("Submit Feedback", key=f"submit_{bill_id_safe}", type="primary"):
947
+ append_feedback_row(
948
+ bill_id=bill_id_safe,
949
+ predicted_impact=predicted_impact,
950
+ user_response="No",
951
+ corrected_impact=corrected_value,
952
+ comment=comment,
953
+ )
954
+ st.session_state[f"feedback_done_{bill_id_safe}"] = True
955
+ st.session_state[f"show_corrected_{bill_id_safe}"] = False
956
+ st.rerun()
957
+
958
+ with col_cancel:
959
+ if st.button("Cancel", key=f"cancel_{bill_id_safe}"):
960
+ st.session_state[f"show_corrected_{bill_id_safe}"] = False
961
+ st.rerun()
962
+
963
+ st.markdown("---")
964
+
965
+
966
+ # =========================================================
967
+ # TRENDS TAB
968
+ # =========================================================
969
+ with tab_trends:
970
+ top_policy = (
971
+ filtered_df[POLICY_COL].value_counts().head(1)
972
+ if POLICY_COL in filtered_df.columns else pd.Series(dtype=int)
973
+ )
974
+
975
+ top_beneficiaries = (
976
+ filtered_df[BENEFICIARY_COL].value_counts().head(1)
977
+ if BENEFICIARY_COL in filtered_df.columns else pd.Series(dtype=int)
978
+ )
979
+
980
+ strategy_impact = (
981
+ filtered_df[filtered_df[IMPACT_COL].notna()]
982
+ .groupby(STRATEGY_COL)[IMPACT_COL]
983
+ .apply(lambda x: (x == "Very Impactful").sum())
984
+ if STRATEGY_COL in filtered_df.columns and IMPACT_COL in filtered_df.columns
985
+ else pd.Series(dtype=int)
986
+ )
987
+
988
+ avg_impact_ben = (
989
+ filtered_df.dropna(subset=["impact_rating_score"])
990
+ .groupby(BENEFICIARY_COL)["impact_rating_score"]
991
+ .mean()
992
+ .sort_values(ascending=False)
993
+ if BENEFICIARY_COL in filtered_df.columns and "impact_rating_score" in filtered_df.columns
994
+ else pd.Series(dtype=float)
995
+ )
996
+
997
+ total_bills = len(filtered_df)
998
+ total_high_impact = (
999
+ (filtered_df[IMPACT_COL] == "Very Impactful").sum()
1000
+ if IMPACT_COL in filtered_df.columns else 0
1001
+ )
1002
+
1003
+ st.markdown("### Key Insights")
1004
+ st.write(f"**Total Bills Considered:** {total_bills}")
1005
+ st.write(f"**Total Very Impactful Bills:** {total_high_impact}")
1006
+ st.write(
1007
+ f"**Most Active Policy Domain:** {top_policy.index[0]} ({top_policy.iloc[0]} bills)"
1008
+ if not top_policy.empty else "No data"
1009
+ )
1010
+ st.write(
1011
+ f"**Most Benefited Group:** {top_beneficiaries.index[0]} ({top_beneficiaries.iloc[0]} bills)"
1012
+ if not top_beneficiaries.empty else "No data"
1013
+ )
1014
+ st.write(
1015
+ f"**Strategy Producing Most Very Impactful Bills:** {strategy_impact.idxmax()}"
1016
+ if not strategy_impact.empty else "N/A"
1017
+ )
1018
+ st.write(
1019
+ f"**Highest Average Impact (Beneficiary):** {avg_impact_ben.index[0]} ({avg_impact_ben.iloc[0]:.2f})"
1020
+ if not avg_impact_ben.empty else "N/A"
1021
+ )
1022
+ st.markdown("---")
1023
+
1024
+ col1, col2 = st.columns(2)
1025
+
1026
+ with col1:
1027
+ st.markdown("### Policy Domain Activity")
1028
+ if POLICY_COL in filtered_df.columns:
1029
+ policy_agg = (
1030
+ filtered_df.groupby(POLICY_COL)
1031
+ .agg(
1032
+ Count=("bill_id", "count"),
1033
+ avg_impact=("impact_rating_score", "mean")
1034
+ if "impact_rating_score" in filtered_df.columns else ("bill_id", "count"),
1035
+ top_bills=("title", lambda x: "; ".join(x.head(5))),
1036
+ top_beneficiaries=(
1037
+ BENEFICIARY_COL,
1038
+ lambda x: ", ".join(x.value_counts().head(3).index)
1039
+ ) if BENEFICIARY_COL in filtered_df.columns else ("title", lambda x: ""),
1040
+ recent_date=(
1041
+ DATE_COL,
1042
+ lambda x: x.max().strftime("%Y-%m-%d") if pd.notna(x.max()) else ""
1043
+ ) if DATE_COL in filtered_df.columns else ("title", lambda x: ""),
1044
+ bill_numbers=("bill_number", lambda x: ", ".join(map(str, x.head(5))))
1045
+ )
1046
+ .reset_index()
1047
+ .rename(columns={POLICY_COL: "Policy Domain"})
1048
+ )
1049
+
1050
+ policy_chart = (
1051
+ alt.Chart(policy_agg)
1052
+ .mark_bar()
1053
+ .encode(
1054
+ x=alt.X("Policy Domain:N", sort="-y", title="Policy Domain"),
1055
+ y=alt.Y("Count:Q", title="Number of Bills"),
1056
+ color=alt.Color(
1057
+ "avg_impact:Q",
1058
+ title="Average Impact",
1059
+ scale=alt.Scale(scheme="orangered")
1060
+ ),
1061
+ tooltip=[
1062
+ alt.Tooltip("Policy Domain:N"),
1063
+ alt.Tooltip("Count:Q", title="Number of Bills"),
1064
+ alt.Tooltip("avg_impact:Q", format=".2f", title="Average Impact"),
1065
+ alt.Tooltip("top_bills:N", title="Top Bills"),
1066
+ alt.Tooltip("top_beneficiaries:N", title="Top Beneficiaries"),
1067
+ alt.Tooltip("recent_date:N", title="Most Recent Bill"),
1068
+ alt.Tooltip("bill_numbers:N", title="Bill Numbers")
1069
+ ]
1070
+ )
1071
+ .properties(height=400)
1072
+ )
1073
+ st.altair_chart(policy_chart, use_container_width=True)
1074
+ else:
1075
+ st.write("No policy domain data available.")
1076
+
1077
+ with col2:
1078
+ st.markdown("### Impact Distribution")
1079
+ if IMPACT_COL in filtered_df.columns:
1080
+ impact_hover = (
1081
+ filtered_df[filtered_df[IMPACT_COL].notna()]
1082
+ .groupby(IMPACT_COL)
1083
+ .agg(
1084
+ Count=("bill_id", "count"),
1085
+ top_beneficiaries=(
1086
+ BENEFICIARY_KEYWORD_COL,
1087
+ lambda x: ", ".join(x.dropna().astype(str).value_counts().head(3).index)
1088
+ ) if BENEFICIARY_KEYWORD_COL in filtered_df.columns else ("bill_id", lambda x: ""),
1089
+ top_motivation=(
1090
+ MOTIVATION_COL,
1091
+ lambda x: ", ".join(x.dropna().astype(str).value_counts().head(3).index)
1092
+ ) if MOTIVATION_COL in filtered_df.columns else ("bill_id", lambda x: "")
1093
+ )
1094
+ .reindex(IMPACT_ORDER)
1095
+ .reset_index()
1096
+ )
1097
+
1098
+ impact_chart = (
1099
+ alt.Chart(impact_hover)
1100
+ .mark_bar()
1101
+ .encode(
1102
+ x=alt.X(f"{IMPACT_COL}:N", sort=IMPACT_ORDER, title="Impact Level"),
1103
+ y=alt.Y("Count:Q"),
1104
+ color=alt.Color("Count:Q", scale=alt.Scale(scheme="reds"), legend=None),
1105
+ tooltip=[
1106
+ alt.Tooltip(f"{IMPACT_COL}:N", title="Impact Level"),
1107
+ alt.Tooltip("Count:Q", title="Count"),
1108
+ alt.Tooltip("top_beneficiaries:N", title="Top Beneficiaries"),
1109
+ alt.Tooltip("top_motivation:N", title="Top Motivation")
1110
+ ]
1111
+ )
1112
+ .properties(height=300)
1113
+ )
1114
+ st.altair_chart(impact_chart, use_container_width=True)
1115
+ else:
1116
+ st.write("No impact rating data available.")
1117
+
1118
+ st.markdown("### Legislative Strategy: Very Impactful Bills")
1119
+ if STRATEGY_COL in filtered_df.columns and IMPACT_COL in filtered_df.columns:
1120
+ strategy_high_impact = (
1121
+ filtered_df[filtered_df[IMPACT_COL].notna()]
1122
+ .groupby(STRATEGY_COL)
1123
+ .agg(
1124
+ Very_Impactful_Bills=(IMPACT_COL, lambda x: (x == "Very Impactful").sum()),
1125
+ top_bills=("title", lambda x: "; ".join(x.head(5))),
1126
+ top_beneficiaries=(
1127
+ BENEFICIARY_COL,
1128
+ lambda x: ", ".join(x.value_counts().head(3).index)
1129
+ ) if BENEFICIARY_COL in filtered_df.columns else ("title", lambda x: ""),
1130
+ recent_date=(
1131
+ DATE_COL,
1132
+ lambda x: x.max().strftime("%Y-%m-%d") if pd.notna(x.max()) else ""
1133
+ ) if DATE_COL in filtered_df.columns else ("title", lambda x: "")
1134
+ )
1135
+ .reset_index()
1136
+ .rename(columns={STRATEGY_COL: "Strategy"})
1137
+ )
1138
+
1139
+ strategy_chart = (
1140
+ alt.Chart(strategy_high_impact)
1141
+ .mark_bar()
1142
+ .encode(
1143
+ x=alt.X("Strategy:N", sort="-y", title="Strategy"),
1144
+ y=alt.Y("Very_Impactful_Bills:Q", title="Very Impactful Bills"),
1145
+ color=alt.Color("Very_Impactful_Bills:Q", scale=alt.Scale(scheme="orangered")),
1146
+ tooltip=[
1147
+ alt.Tooltip("Strategy:N"),
1148
+ alt.Tooltip("Very_Impactful_Bills:Q"),
1149
+ alt.Tooltip("top_bills:N", title="Top Bills"),
1150
+ alt.Tooltip("top_beneficiaries:N", title="Top Beneficiaries"),
1151
+ alt.Tooltip("recent_date:N", title="Most Recent Bill")
1152
+ ]
1153
+ )
1154
+ .properties(height=400)
1155
+ )
1156
+ st.altair_chart(strategy_chart, use_container_width=True)
1157
+ else:
1158
+ st.write("No legislative strategy data available for selected filters.")
1159
+
1160
+ st.markdown("### Beneficiary Coverage & Average Impact")
1161
+ if BENEFICIARY_COL in filtered_df.columns and "impact_rating_score" in filtered_df.columns:
1162
+ ben_df = (
1163
+ filtered_df.dropna(subset=[BENEFICIARY_COL, "impact_rating_score"])
1164
+ .groupby(BENEFICIARY_COL)
1165
+ .agg(
1166
+ total_bills=("bill_id", "count"),
1167
+ avg_impact=("impact_rating_score", "mean"),
1168
+ top_bills=("title", lambda x: "; ".join(x.head(5))),
1169
+ recent_date=(
1170
+ DATE_COL,
1171
+ lambda x: x.max().strftime("%Y-%m-%d") if pd.notna(x.max()) else ""
1172
+ ) if DATE_COL in filtered_df.columns else ("title", lambda x: ""),
1173
+ bill_numbers=("bill_number", lambda x: ", ".join(map(str, x.head(5))))
1174
+ )
1175
+ .reset_index()
1176
+ )
1177
+
1178
+ if not ben_df.empty:
1179
+ ben_chart = (
1180
+ alt.Chart(ben_df)
1181
+ .mark_rect()
1182
+ .encode(
1183
+ x=alt.X("total_bills:Q", title="Number of Bills"),
1184
+ y=alt.Y(f"{BENEFICIARY_COL}:N", sort="-x", title="Beneficiary Group"),
1185
+ color=alt.Color(
1186
+ "avg_impact:Q",
1187
+ scale=alt.Scale(domain=[0, 3], range=["#fde68a", "#dc2626"]),
1188
+ legend=alt.Legend(title="Average Impact Score")
1189
+ ),
1190
+ tooltip=[
1191
+ alt.Tooltip(f"{BENEFICIARY_COL}:N", title="Beneficiary"),
1192
+ alt.Tooltip("total_bills:Q", title="Number of Bills"),
1193
+ alt.Tooltip("avg_impact:Q", format=".2f", title="Average Impact"),
1194
+ alt.Tooltip("top_bills:N", title="Top Bills"),
1195
+ alt.Tooltip("recent_date:N", title="Most Recent Bill"),
1196
+ alt.Tooltip("bill_numbers:N", title="Bill Numbers")
1197
+ ]
1198
+ )
1199
+ .properties(height=400)
1200
+ )
1201
+ st.altair_chart(ben_chart, use_container_width=True)
1202
+ else:
1203
+ st.write("No beneficiary impact data available for selected filters.")
1204
+ else:
1205
+ st.write("No beneficiary data available for selected filters.")
1206
+
config.json ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {
2
+ "embedding_model_name": "all-MiniLM-L6-v2",
3
+ "text_column": "Llama Summary",
4
+ "id_column": "bill_id",
5
+ "embedding_dimension": 384,
6
+ "top_k_default": 10
7
+ }
faiss_index.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:597e7443b1da3eaa61fc90530bf797631b6b2fe5f88d7db122cafaf5cb277d4b
3
+ size 72878637
gitattributes ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
gitattributes (2) ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ faiss_index.bin filter=lfs diff=lfs merge=lfs -text
2
+ metadata.parquet filter=lfs diff=lfs merge=lfs -text
metadata.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ed0fe8ca7e6b3641255006ad77a0bb65399901cf5c2537285b909b5e775cbbc6
3
+ size 564127298
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ streamlit
2
+ pandas
3
+ numpy
4
+ faiss-cpu
5
+ sentence-transformers
6
+ pyarrow