Nelly-43 commited on
Commit
585c3ec
·
1 Parent(s): 27527c9

Local updates

Browse files
.gitignore CHANGED
@@ -18,4 +18,7 @@ config.yaml
18
  .ipynb_checkpoints
19
  llm_rag
20
  *.ipynb
21
- .git*
 
 
 
 
18
  .ipynb_checkpoints
19
  llm_rag
20
  *.ipynb
21
+ .git*
22
+ *.csv
23
+ ug
24
+ .cache
app_dev.py ADDED
@@ -0,0 +1,242 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import shutil
2
+ import os
3
+ import streamlit as st
4
+ from huggingface_hub import snapshot_download
5
+
6
+ from src.config_loader import load_config, get_api_key
7
+ from src.ingest import get_chroma_collection, ingest_documents
8
+ from src.retriever import clear_collection_cache, retrieve
9
+ from src.llm import list_models
10
+ from src.peri import APPROVED_OPTIONS
11
+ from src.query_engine import understand_query, categorize_query, init_query, analyze_committment_to_vc
12
+ from src.prompts import dict_to_string
13
+
14
+ snapshot_download(repo_id="CGIAR/peri-kb",
15
+ repo_type="dataset",
16
+ allow_patterns="ug/*",
17
+ token=os.getenv('HF_TOKEN'),
18
+ local_dir="./"
19
+ )
20
+ shutil.copytree("./ug", "./", dirs_exist_ok=True)
21
+
22
+
23
+ def render_sidebar():
24
+ """Render sidebar with provider/model selectors, web search toggle, and KB stats."""
25
+ cfg = st.session_state.cfg
26
+
27
+ with st.sidebar:
28
+ st.header("Settings")
29
+
30
+ # --- Provider dropdown ---
31
+ providers = ["openai", "anthropic", "gemini", "meta-llama"]
32
+ current_provider = cfg.get("llm", {}).get("provider", "openai")
33
+ provider_index = providers.index(current_provider) if current_provider in providers else 0
34
+
35
+ provider = st.selectbox(
36
+ "LLM Provider",
37
+ providers,
38
+ index=provider_index,
39
+ key="sidebar_provider",
40
+ )
41
+
42
+ # Update cfg in session when provider changes
43
+ if provider != cfg.get("llm", {}).get("provider"):
44
+ cfg.setdefault("llm", {})["provider"] = provider
45
+
46
+ # --- Model dropdown (cached per provider) ---
47
+ # Invalidate model cache if provider changed
48
+ prev_provider_key = "prev_provider"
49
+ if st.session_state.get(prev_provider_key) != provider:
50
+ for p in providers:
51
+ st.session_state.pop(f"models_{p}", None)
52
+ st.session_state[prev_provider_key] = provider
53
+
54
+ models_cache_key = f"models_{provider}"
55
+ if models_cache_key not in st.session_state:
56
+ api_key = get_api_key(cfg, provider)
57
+ if api_key:
58
+ try:
59
+ st.session_state[models_cache_key] = list_models(provider, api_key)
60
+ except Exception:
61
+ st.session_state[models_cache_key] = []
62
+ else:
63
+ st.session_state[models_cache_key] = []
64
+
65
+ available_models = st.session_state[models_cache_key]
66
+ current_model = cfg.get("llm", {}).get("model", "")
67
+
68
+ if available_models:
69
+ model_index = (
70
+ available_models.index(current_model)
71
+ if current_model in available_models
72
+ else 0
73
+ )
74
+ model = st.selectbox(
75
+ "Model",
76
+ available_models,
77
+ index=model_index,
78
+ key="sidebar_model",
79
+ )
80
+ else:
81
+ model = st.text_input(
82
+ "Model",
83
+ value=current_model,
84
+ key="sidebar_model_text",
85
+ )
86
+
87
+ # Update cfg in session when model changes
88
+ if model != cfg.get("llm", {}).get("model"):
89
+ cfg.setdefault("llm", {})["model"] = model
90
+
91
+ # --- Web search toggle ---
92
+ web_enabled = cfg.get("web_search", {}).get("enabled", False)
93
+ web_toggle = st.toggle("Web search", value=web_enabled, key="sidebar_web_search")
94
+ cfg.setdefault("web_search", {})["enabled"] = web_toggle
95
+
96
+ st.divider()
97
+
98
+ # --- Knowledge base stats ---
99
+ st.subheader("Knowledge Base")
100
+ try:
101
+ collection = get_chroma_collection(cfg)
102
+ chunk_count = collection.count()
103
+ st.metric("Chunks indexed", chunk_count)
104
+ except Exception as e:
105
+ st.warning(f"Could not read knowledge base: {e}")
106
+ chunk_count = 0
107
+
108
+ # --- Re-ingest button ---
109
+ if st.button("Re-ingest documents", use_container_width=True):
110
+ st.info("Ingestion may take a few minutes for large document collections...")
111
+ with st.spinner("Ingesting documents..."):
112
+ try:
113
+ count = ingest_documents(cfg)
114
+ clear_collection_cache()
115
+ st.success(f"Ingested {count} chunks.")
116
+ # Clear cached data so it refreshes after re-ingest
117
+ st.session_state.pop("kb_welcome_summary", None)
118
+ st.rerun()
119
+ except Exception as e:
120
+ st.error(f"Ingestion failed: {e}")
121
+
122
+
123
+ def render_chat():
124
+ """Render the chat interface with message history and input."""
125
+ cfg = st.session_state.cfg
126
+
127
+ # Display chat history
128
+ for msg in st.session_state.messages:
129
+ with st.chat_message(msg["role"]):
130
+ st.markdown(msg["content"])
131
+
132
+ # Chat input
133
+ user_input = st.chat_input("Ask a question about your knowledge base...",
134
+ accept_file="multiple",
135
+ file_type=["docx", "csv", "xlsx", "xls", "pdf", "rds", "rda",
136
+ "tsv", "sav", "dta", "txt", "md", "json", "do"]
137
+ )
138
+
139
+ if user_input:
140
+ prompt = user_input.text
141
+ uploaded_files = user_input.files
142
+ # Show and store user message
143
+ st.session_state.messages.append({"role": "user", "content": prompt})
144
+ with st.chat_message("user"):
145
+ st.markdown(prompt)
146
+
147
+ # ── Categorize user query ───────────────────
148
+ save_dir = "uploaded_do_files"
149
+ os.makedirs(save_dir, exist_ok=True)
150
+
151
+ # Exclude the just-appended user message to avoid sending
152
+ # the current question twice (once in history, once as query)
153
+ cat_cfg = cfg.get("query_categorization", {})
154
+ max_history = cat_cfg.get("max_history", 6)
155
+
156
+ # ── Query understanding ─────────────────────────────────────────
157
+ qu_cfg = cfg.get("query_understanding", {})
158
+ qu_enabled = qu_cfg.get("enabled", True)
159
+ max_history = qu_cfg.get("max_history", 6)
160
+
161
+ # ── Check if this is a clarification response ───────────────────
162
+ unresolved = st.session_state.unresolved_category
163
+ if unresolved is not None:
164
+ # This prompt is the user's clarification answer
165
+ # Include the clarification question for context
166
+ unresolved_question = st.session_state.get("unresolved_category_question", "")
167
+ if unresolved_question:
168
+ combined_cat = f"{unresolved} (Clarification: Q: {unresolved_question} A: {prompt})"
169
+ else:
170
+ combined_cat = f"{unresolved} — {prompt}"
171
+ st.session_state.unresolved_category_question = None
172
+ original_query_cat = unresolved
173
+ st.session_state.unresolved_category = None
174
+ else:
175
+ combined_cat = prompt
176
+ original_query_cat = prompt
177
+ st.session_state.resolution_rounds = 0
178
+ unresolved_question = []
179
+
180
+ prior_messages = st.session_state.messages[:-1]
181
+ history = [
182
+ {"role": m["role"], "content": m["content"]}
183
+ for m in prior_messages[-max_history:]
184
+ ]
185
+ try:
186
+ qinit_result = init_query(combined_cat, cfg, history)
187
+ print("result: ", qinit_result)
188
+ if qinit_result.get("country", None) is None:
189
+
190
+ with st.chat_message("assistant"):
191
+ st.markdown("Please specify the country for which you'd like to run a PERI analysis")
192
+ return
193
+ if qinit_result.get("value_chain", None) is None and qinit_result.get("investment", None) is None:
194
+
195
+ with st.chat_message("assistant"):
196
+ st.markdown(f"Please specify the value chain or investment area in {qinit_result.get('country', None)} for which you'd like to run a PERI analysis")
197
+ return
198
+ qcat_result = categorize_query(combined_cat, cfg, history)
199
+ print("result: ", qcat_result)
200
+ except Exception as e:
201
+ print(e)
202
+ qinit_result = {"country": None, "value_chain": None, "investment": None}
203
+ qcat_result = {"category": "pillar_1", "action": "unresolved"}
204
+
205
+ if not isinstance(qinit_result.get("country", None), list) and qinit_result.get("country", None)==None:
206
+ resolution_msg = f"It seems there is no country specified for the analysis. Currently the PERI framework supports analysis for the countries listed below:\n\n {', '.join([c.capitalize() for c in APPROVED_OPTIONS.get('countries')])}\n\n We are also continuously \
207
+ working to expand the framework and you can submit a form if the country you would like to run the analysis on is not included. In the meantime please let me know if you would like to run the anlysis for one of the included countries."
208
+
209
+ if qinit_result.get("country", None)[0].lower() not in [c.lower() for c in APPROVED_OPTIONS.get('countries')]:
210
+ resolution_msg = f"It seems you are trying to run a PERI analysis for {qinit_result.get('country', None)[0].capitalize()}! Currently the PERI framework only supports analysis for the countries listed below:\n\n {', '.join([c.capitalize() for c in APPROVED_OPTIONS.get('countries')])}\n\n We are continuously \
211
+ working to expand the framework and you can submit a form to request. In the meantime please let me know if you would like to run the anlysis for one of the included countries."
212
+
213
+ if qinit_result.get("value_chain", None)[0]==None and qinit_result.get('investment', None)[0]==None:
214
+ resolution_msg = f"It seems there is no value chain or investment area specified for the analysis. Please select one of the value chains or investment areas included in the current PERI framework."
215
+
216
+ if qinit_result.get("value_chain", None)[0].lower() not in [c.lower() for v in APPROVED_OPTIONS.get('value_chains').values() for c in v]:
217
+ resolution_msg = f"It seems you are trying to run a PERI analysis for {qinit_result.get('value_chain', None)[0]} in {qinit_result.get('country', None)[0].capitalize()}! Currently the PERI framework only supports analysis for the value chains listed below:\n\n {dict_to_string(APPROVED_OPTIONS.get('value_chains'), 2)}\n\n We are continuously \
218
+ working to expand the framework and you can submit a form to request. In the meantime please let me know if you would like to run the anlysis for one of the included countries."
219
+
220
+ if qinit_result.get("investment", None)[0].lower() not in [i.lower() for i in APPROVED_OPTIONS.get('investments')]:
221
+ resolution_msg = f"It seems you are trying to run a PERI analysis for {qinit_result.get('investment', None)[0]} investemnt in {qinit_result.get('country', None)[0].capitalize()}! Currently the PERI framework only supports analysis for the investment areas listed below:\n\n {', '.join([c.capitalize() for c in APPROVED_OPTIONS.get('investments')])}\n\n We are continuously \
222
+ working to expand the framework and you can submit a form to request. In the meantime please let me know if you would like to run the anlysis for one of the included countries."
223
+
224
+ if qinit_result.get("country", None)[0] in APPROVED_OPTIONS.get('countries') and (qinit_result.get('value_chain', None)[0] in APPROVED_OPTIONS.get('value_chains') or qinit_result.get('investment', None)[0] in APPROVED_OPTIONS.get('investments')):
225
+
226
+ pillar_dict = {
227
+ "pillar_1":f"would like to understand whether {','.join(qinit_result.get('value_chain'))} aligns with the government's political incentives.",
228
+ "pillar_2":f"would like to understand to what degree decisions are impacted by the lobbying of particular groups or by elite influence",
229
+ "pillar_3":f"would like to understand if {','.join(qinit_result.get('value_chain'))} and/or investing in {','.join(qinit_result.get('investment'))} can be feasibly implemented given the broader institutional and policy environment",
230
+
231
+ }
232
+ resolution_msg = f"**Before I search, could you clarify?** Please let me know if you {' and '.join([pillar_dict[c] for c in qcat_result.get('category')])}."
233
+
234
+ # st.session_state.unresolved_category_question = f"Please let me know if you {' and '.join([pillar_dict[c] for c in qcat_result.get('category')])}."
235
+ st.session_state.unresolved_category_question = resolution_msg
236
+ st.session_state.unresolved_category = original_query_cat
237
+ st.session_state.resolution_rounds += 1
238
+ st.session_state.messages.append({"role": "assistant", "content": resolution_msg})
239
+
240
+ with st.chat_message("assistant"):
241
+ st.markdown(resolution_msg)
242
+ return
app_web.py CHANGED
@@ -3,18 +3,21 @@ import os
3
  import shutil
4
  import zipfile
5
  import streamlit as st
 
6
 
7
  from src.config_loader import load_config, get_api_key
8
  from src.ingest import get_chroma_collection, ingest_documents
9
  from src.kb_meta import load_kb_meta_brief
10
- from src.query_engine import understand_query, categorize_query, init_query, analyze_committment_to_vc
11
  from src.retriever import clear_collection_cache, retrieve
12
  from src.verifier import verify_and_respond
13
  from src.llm import list_models
14
  from src.stata import explain_
15
  from src.peri import APPROVED_OPTIONS
 
 
16
  from src.prompts import dict_to_string
17
-
18
  from huggingface_hub import snapshot_download
19
 
20
  snapshot_download(repo_id="CGIAR/peri-kb",
@@ -28,6 +31,8 @@ shutil.copytree("./ug", "./", dirs_exist_ok=True)
28
 
29
  def init_session():
30
  """Initialise st.session_state with messages list, cfg, and last_retrieval."""
 
 
31
  if "messages" not in st.session_state:
32
  st.session_state.messages = []
33
  if "cfg" not in st.session_state:
@@ -55,6 +60,175 @@ def init_session():
55
  if "stata_file" not in st.session_state:
56
  st.session_state.stata_file = None
57
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
 
59
  # ── Sidebar ──────────────────────────────────────────────────────────────────
60
 
@@ -528,7 +702,70 @@ def render_chat():
528
  if len(st.session_state.messages) > max_messages:
529
  st.session_state.messages = st.session_state.messages[-max_messages:]
530
 
531
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
532
  # ── Main ─────────────────────────────────────────────────────────────────────
533
 
534
  def main():
@@ -558,8 +795,13 @@ def main():
558
  please let me know what pillar(s) you'd like us to analyze today and for what country. If this is\
559
  is your first time working with PERI please proceed by letting me know what your particular interests are.")
560
 
561
- render_sidebar()
562
- render_chat()
 
 
 
 
 
563
 
564
  st.caption(
565
  "All answers are sourced from the local knowledge base. "
 
3
  import shutil
4
  import zipfile
5
  import streamlit as st
6
+ import pandas as pd
7
 
8
  from src.config_loader import load_config, get_api_key
9
  from src.ingest import get_chroma_collection, ingest_documents
10
  from src.kb_meta import load_kb_meta_brief
11
+ from src.query_engine import understand_query, categorize_query, init_query, _parse_vc_analyzer_result
12
  from src.retriever import clear_collection_cache, retrieve
13
  from src.verifier import verify_and_respond
14
  from src.llm import list_models
15
  from src.stata import explain_
16
  from src.peri import APPROVED_OPTIONS
17
+ from src.peri.value_chains import categorize_vc_production, analyze_committment_to_vc
18
+ from src.peri.investments import analyze_investments
19
  from src.prompts import dict_to_string
20
+ # from ui import render_db
21
  from huggingface_hub import snapshot_download
22
 
23
  snapshot_download(repo_id="CGIAR/peri-kb",
 
31
 
32
  def init_session():
33
  """Initialise st.session_state with messages list, cfg, and last_retrieval."""
34
+ if "bot_launched" not in st.session_state:
35
+ st.session_state.bot_launched = False
36
  if "messages" not in st.session_state:
37
  st.session_state.messages = []
38
  if "cfg" not in st.session_state:
 
60
  if "stata_file" not in st.session_state:
61
  st.session_state.stata_file = None
62
 
63
+ # ── Sidebar ──────────────────────────────────────────────────────────────────
64
+ def render_landing_page():
65
+ cfg = st.session_state.cfg
66
+ # print(cfg)
67
+ st.title(f"🤖 Welcome to the {cfg["chatbot"].get("name")} Assistant!")
68
+ st.markdown("Let's go through some preliminary steps below before getting started.")
69
+
70
+ st.divider()
71
+
72
+ # Dropdown Options
73
+ country = st.selectbox(
74
+ "Please select a country you would like to prepare a PERI analysis for today:",
75
+ [None]+[c.capitalize() for c in APPROVED_OPTIONS.get("countries")],
76
+ format_func=lambda x: "Please select a country..." if x is None else x
77
+ )
78
+
79
+ regime = None
80
+ if country is not None:
81
+ regime = st.selectbox(
82
+ f"Please specify whether {country.capitalize()} is an autocracy or democracy:",
83
+ [None, "Autocracy", "Democracy"],
84
+ format_func=lambda x: f"Please specify {country.capitalize()}'s regime..." if x is None else x
85
+ )
86
+
87
+ # creativity_level = st.select_slider(
88
+ # "Adjust Creativity (Temperature):",
89
+ # options=["Low (Factual)", "Medium (Balanced)", "High (Creative)"],
90
+ # value="Medium (Balanced)"
91
+ # )
92
+
93
+ uploaded_ref = None
94
+ if regime is not None:
95
+ # Create the file uploader widget
96
+ pol_geo_desc = """\
97
+ A PERI analysis typically requires a preliminary analysis of
98
+ the political geography of the country under review. Please upload
99
+ the political geography analysis results for {country} if available.
100
+ You can also follow the guide at ... to prepare the analysis and upload your results:
101
+ """
102
+ st.write(pol_geo_desc.format(country=country))
103
+ uploaded_ref = st.file_uploader(
104
+ f"Please upload the political geography analysis results for {country}",
105
+ type=["csv", "xls", "xlsx"])
106
+
107
+ if uploaded_ref is not None:
108
+ st.divider()
109
+
110
+ # Launch Button
111
+ if st.button("Launch PERI-AI 🚀", type="primary", use_container_width=True):
112
+ # Save selections to session state to use during the chat session
113
+ try:
114
+ pol_geo_ref = pd.read_csv(uploaded_ref)
115
+ except Exception:
116
+ pol_geo_ref = pd.read_excel(uploaded_ref)
117
+
118
+ st.session_state.country = country
119
+ st.session_state.regime = regime
120
+ st.session_state.pol_geo_ref = pol_geo_ref
121
+
122
+ with st.status("Preparing metadata for the analysis...", expanded=True) as status:
123
+ status.update(label=f'Preparing value chain metadata...')
124
+ pol_geo_scores = categorize_vc_production(pol_geo_ref, "value_chain")
125
+
126
+ status.update(label=f'Preparing investment area metadata...')
127
+ investments = analyze_investments()
128
+
129
+ # ── Query understanding ─────────────────────────────────────────
130
+ qu_cfg = cfg.get("query_understanding", {})
131
+ qu_enabled = qu_cfg.get("enabled", True)
132
+ max_history = qu_cfg.get("max_history", 6)
133
+
134
+ prior_messages = st.session_state.messages[:-1]
135
+ history = [
136
+ {"role": m["role"], "content": m["content"]}
137
+ for m in prior_messages[-max_history:]
138
+ ]
139
+
140
+ c2vc = {}
141
+ for vc in pol_geo_ref["value_chain"].unique():
142
+ status.update(label=f'Analyzing committment to {vc} production...')
143
+
144
+ prompt = f"{vc} production in uganda. use data tables and statistics where possible"
145
+ st.session_state.messages.append({"role": "user", "content": prompt})
146
+
147
+ # ── Check if this is a clarification response ───────────────────
148
+ pending = st.session_state.pending_clarification
149
+ if pending is not None:
150
+ # This prompt is the user's clarification answer
151
+ # Include the clarification question for context
152
+ pending_question = st.session_state.get("pending_clarification_question", "")
153
+ if pending_question:
154
+ combined = f"{pending} (Clarification: Q: {pending_question} A: {prompt})"
155
+ else:
156
+ combined = f"{pending} — {prompt}"
157
+ st.session_state.pending_clarification_question = None
158
+ original_query = pending
159
+ st.session_state.pending_clarification = None
160
+ else:
161
+ combined = prompt
162
+ original_query = prompt
163
+ st.session_state.clarification_rounds = 0
164
+
165
+ search_query = combined
166
+ display_query = combined
167
+ route = "vector"
168
+ sql_query = None
169
+ max_clarifications = qu_cfg.get("max_clarifications", 1)
170
+
171
+ if qu_enabled:
172
+ print("Q understanding")
173
+ # Exclude the just-appended user message to avoid sending
174
+ # the current question twice (once in history, once as query)
175
+ prior_messages = st.session_state.messages[:-1]
176
+ history = [
177
+ {"role": m["role"], "content": m["content"]}
178
+ for m in prior_messages[-max_history:]
179
+ ]
180
+ try:
181
+ qu_result = understand_query(combined, cfg, history)
182
+ except Exception:
183
+ qu_result = {"action": "search", "search_query": combined, "display_query": combined, "original_query": original_query, "route": "vector", "sql_query": None}
184
+
185
+ if qu_result.get("action") == "clarify" and st.session_state.clarification_rounds < max_clarifications:
186
+ # Ask clarification — store original query and question for context
187
+ st.session_state.pending_clarification = original_query
188
+ st.session_state.pending_clarification_question = qu_result.get('clarification_question', 'Could you be more specific?')
189
+ st.session_state.clarification_rounds += 1
190
+ clarification_msg = f"**Before I search, could you clarify?** {qu_result.get('clarification_question', 'Could you be more specific?')}"
191
+ st.session_state.messages.append(
192
+ {"role": "assistant", "content": clarification_msg}
193
+ )
194
+ with st.chat_message("assistant"):
195
+ st.markdown(clarification_msg)
196
+ return
197
+
198
+ # After max clarification rounds, force search (matches CLI behavior)
199
+ if qu_result.get("action") == "clarify":
200
+ qu_result["action"] = "search"
201
+
202
+ search_query = qu_result.get("search_query", combined)
203
+ display_query = qu_result.get("display_query", original_query)
204
+ route = qu_result.get("route", "vector")
205
+ sql_query = qu_result.get("sql_query")
206
+
207
+ retrieval_result = retrieve(search_query, cfg, route=route, sql_query=sql_query)
208
+ st.session_state.last_retrieval = retrieval_result
209
+
210
+ output = verify_and_respond(
211
+ display_query, retrieval_result, cfg, vc, original_query,
212
+ )
213
+ print(route)
214
+ print(sql_query)
215
+ print(display_query)
216
+ print(output["response"])
217
+ c2vc.update(_parse_vc_analyzer_result(output["response"]))
218
+
219
+ status.update(
220
+ label=f"Completed metadata preparation for {pol_geo_scores.shape[0]} value chains and {investments.shape[0]} investment areas in {country}.",
221
+ state="complete",
222
+ )
223
+ print(c2vc)
224
+ st.session_state.beneficiaries_var = pol_geo_scores
225
+ st.session_state.investments_var = investments
226
+ st.session_state.c2vc = c2vc
227
+ st.session_state.country = country
228
+ st.session_state.bot_launched = True
229
+ st.rerun()
230
+
231
+
232
 
233
  # ── Sidebar ──────────────────────────────────────────────────────────────────
234
 
 
702
  if len(st.session_state.messages) > max_messages:
703
  st.session_state.messages = st.session_state.messages[-max_messages:]
704
 
705
+ def render_db():
706
+ # rows = st.columns((5,5), gap='medium')
707
+ row1 = st.columns((2,6), gap='medium')
708
+ row2 = st.container()
709
+ with row1[0]:
710
+ st.markdown(f'#### Political Geography of {st.session_state.country}')
711
+ st.dataframe(st.session_state.beneficiaries_var,
712
+ # column_order=("states", "population"),
713
+ hide_index=True,
714
+ width='stretch',
715
+ # column_config={
716
+ # "states": st.column_config.TextColumn(
717
+ # "States",
718
+ # ),
719
+ # "population": st.column_config.ProgressColumn(
720
+ # "Population",
721
+ # format="%f",
722
+ # min_value=0,
723
+ # max_value=max(st.session_state.beneficiaries_var.population),
724
+ # )}
725
+ )
726
+
727
+ with row1[1]:
728
+ st.markdown(f'#### Investment Area Analysis Results')
729
+ st.dataframe(st.session_state.investments_var,
730
+ # column_order=("states", "population"),
731
+ hide_index=True,
732
+ width='stretch',
733
+ # column_config={
734
+ # "states": st.column_config.TextColumn(
735
+ # "States",
736
+ # ),
737
+ # "population": st.column_config.ProgressColumn(
738
+ # "Population",
739
+ # format="%f",
740
+ # min_value=0,
741
+ # max_value=max(st.session_state.beneficiaries_var.population),
742
+ # )}
743
+ )
744
+ with row2:
745
+ # with cols[2]:
746
+ st.markdown(f"#### Committment of {st.session_state.country}'s Government to Various Value Chains")
747
+
748
+
749
+ def display_nested_dict(d, indent=0, cols_per_row=2):
750
+ # Get all items in the dictionary
751
+ items = list(d.items())
752
+
753
+ # Create rows of columns
754
+ for i in range(0, len(items), cols_per_row):
755
+ cols = st.columns(cols_per_row)
756
+ for j in range(cols_per_row):
757
+ if i + j < len(items):
758
+ key, value = items[i + j]
759
+ with cols[j]:
760
+ if isinstance(value, dict):
761
+ with st.expander(f"{' ' * indent}📁 {key.capitalize()}"):
762
+ # Recursive call (you can adjust columns or keep single column inside expander)
763
+ display_nested_dict(value, indent + 2, 1)
764
+ else:
765
+ st.write(f"**{key.capitalize()}:** {value}")
766
+
767
+ display_nested_dict(st.session_state.c2vc, cols_per_row=3)
768
+ # st.json(st.session_state.c2vc)
769
  # ── Main ─────────────────────────────────────────────────────────────────────
770
 
771
  def main():
 
795
  please let me know what pillar(s) you'd like us to analyze today and for what country. If this is\
796
  is your first time working with PERI please proceed by letting me know what your particular interests are.")
797
 
798
+ if not st.session_state.bot_launched:
799
+ render_landing_page()
800
+
801
+ else:
802
+ # render_sidebar()
803
+ # render_chat()
804
+ render_db()
805
 
806
  st.caption(
807
  "All answers are sourced from the local knowledge base. "
src/kb_meta.py CHANGED
@@ -259,8 +259,29 @@ def _collect_all_data(collection) -> tuple[list[str], list[dict]]:
259
  """Fetch all documents and metadatas from ChromaDB in a single call."""
260
  if collection.count() == 0:
261
  return [], []
262
- all_data = collection.get(include=["documents", "metadatas"])
263
- return all_data.get("documents", []), all_data.get("metadatas", [])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
264
 
265
 
266
  def collect_file_records(collection, *, _cache: tuple = None) -> list[dict]:
 
259
  """Fetch all documents and metadatas from ChromaDB in a single call."""
260
  if collection.count() == 0:
261
  return [], []
262
+ batch_size = 5000
263
+ offset = 0
264
+ all_documents = []
265
+ all_metadatas = []
266
+
267
+ while True:
268
+ batch = collection.get(
269
+ limit=batch_size,
270
+ offset=offset,
271
+ include=["documents", "metadatas"]
272
+ )
273
+
274
+ # Break when no more records are returned
275
+ if not batch["ids"]:
276
+ break
277
+
278
+ # print(batch)
279
+
280
+ all_documents.extend(batch["documents"])
281
+ all_metadatas.extend(batch["metadatas"])
282
+ offset += batch_size
283
+ # all_data = collection.get(include=["documents", "metadatas"])
284
+ return all_documents, all_metadatas
285
 
286
 
287
  def collect_file_records(collection, *, _cache: tuple = None) -> list[dict]:
src/peri.py DELETED
@@ -1,92 +0,0 @@
1
- COUNTRIES = ["ethiopia", "kenya", "malawi",
2
- "nigeria", "tanzania", "uganda", "zambia"]
3
- ROOT_CROPS = ["potato", "cassava", "sweet_potato"]
4
- CASH_CROPS = ["coffee", "cocoa", "tea",
5
- "tobacco", "cotton", "sugar"]
6
- HORTICULTURE = ["mango", "avocado", "haricot_vert", "banana", "other"]
7
- CEREALS = ["maize", "rice", "wheat", "teff"]
8
- INVESTMENTS = ["agricultural extension", "agricultural research and development", "biosecurity"
9
- "cooperatives", "credit", "fertilizer", "input subsidies", "irrigation",
10
- "machinery", "market information", "pest surveillance", "roads",
11
- "seeds", "storage facilities", "training", "weather surveillance"]
12
-
13
- APPROVED_OPTIONS = {
14
- "countries":COUNTRIES,
15
- "value_chains":{
16
- "root":ROOT_CROPS,
17
- "cash":CASH_CROPS,
18
- "hort":HORTICULTURE,
19
- "cereal":CEREALS,
20
- "other":["livestock and dairy", "fisheries", "pulses and oilseeds"]
21
- },
22
- "investments":INVESTMENTS
23
- }
24
-
25
- tti_ref = {
26
- "within 1 year":["input subsidies", "machinery", "credit"],
27
- "between 1 to 5 years":["small scale irrigation", "storage facilities",
28
- "roads", "market information", "weather surveillance",
29
- "cooperatives", "training"],
30
- "between 5 to 10 years":["large scale irrigation"],
31
- "10 years or more":["agricultural research and development"],
32
- }
33
-
34
- target_ref = {
35
- "private":["fertilizer", "machinery", "credit", "storage facilities"],
36
- "club":["irrigation", "roads", "cooperatives", "storage facilities"],
37
- "quasi public (impure public)":["agricultural extension"],
38
- "public":["agricultural research and development", "pest surveillance",
39
- "biosecurity", "weather surveillance", "market information"],
40
- }
41
-
42
- vis_ref = {
43
- "very high":["roads", "irrigation"],
44
- "high":["input subsidies"],
45
- "medium":["agricultural extension", "credit", "training"],
46
- "low":["weather surveillance", "market information"],
47
- "very low":["agricultural research and development"]
48
- }
49
-
50
- target_add_ref = {
51
- "private":{
52
- "rivalrous":"yes",
53
- "excludable":"yes"
54
- },
55
- "club":{
56
- "rivalrous":"no",
57
- "excludable":"yes"
58
- },
59
- "quasi public (impure public)":{
60
- "rivalrous":"no",
61
- "excludable":"partially"
62
- },
63
- "public":{
64
- "rivalrous":"no",
65
- "excludable":"no"
66
- },
67
- }
68
-
69
- vis_add_ref = {
70
- "very high":{
71
- "desc":"physical infrastructure"
72
- },
73
- "high":{
74
- "desc":"tangible inputs delivered to farmers"
75
- },
76
- "medium":{
77
- "desc":"services or training"
78
- },
79
- "low":{
80
- "desc":"informattional goods"
81
- },
82
- "very low":{
83
- "desc":"knowledge generation, institutional change"
84
- },
85
- }
86
-
87
- def prepare_investment_codings(var_ref:dict, var_col:str=None):
88
- score_ref = {k:v for k,v in zip(var_ref, range(len(var_ref), 0, -1))}
89
- score_df = pd.DataFrame({"investments":[c for v in var_ref.values() for c in v],
90
- var_col:[k for k,v in var_ref.items() for c in v],
91
- "score":[score_ref[k] for k,v in var_ref.items() for c in v]})
92
- return normalize_scores(score_df)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/peri/__init__.py CHANGED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ COUNTRIES = ["ethiopia", "kenya", "malawi",
2
+ "nigeria", "tanzania", "uganda", "zambia"]
3
+ ROOT_CROPS = ["potato", "cassava", "sweet_potato"]
4
+ CASH_CROPS = ["coffee", "cocoa", "tea",
5
+ "tobacco", "cotton", "sugar"]
6
+ HORTICULTURE = ["mango", "avocado", "haricot_vert", "banana", "other"]
7
+ CEREALS = ["maize", "rice", "wheat", "teff"]
8
+ INVESTMENTS = ["agricultural extension", "agricultural research and development", "biosecurity"
9
+ "cooperatives", "credit", "fertilizer", "input subsidies", "irrigation",
10
+ "machinery", "market information", "pest surveillance", "roads",
11
+ "seeds", "storage facilities", "training", "weather surveillance"]
12
+
13
+ APPROVED_OPTIONS = {
14
+ "countries":COUNTRIES,
15
+ "value_chains":{
16
+ "root":ROOT_CROPS,
17
+ "cash":CASH_CROPS,
18
+ "hort":HORTICULTURE,
19
+ "cereal":CEREALS,
20
+ "other":["livestock and dairy", "fisheries", "pulses and oilseeds"]
21
+ },
22
+ "investments":INVESTMENTS
23
+ }
24
+
25
+ tti_ref = {
26
+ "within 1 year":["input subsidies", "machinery", "credit"],
27
+ "between 1 to 5 years":["small scale irrigation", "storage facilities",
28
+ "roads", "market information", "weather surveillance",
29
+ "cooperatives", "training"],
30
+ "between 5 to 10 years":["large scale irrigation"],
31
+ "10 years or more":["agricultural research and development"],
32
+ }
33
+
34
+ target_ref = {
35
+ "private":["fertilizer", "machinery", "credit", "storage facilities"],
36
+ "club":["irrigation", "roads", "cooperatives", "storage facilities"],
37
+ "quasi public (impure public)":["agricultural extension"],
38
+ "public":["agricultural research and development", "pest surveillance",
39
+ "biosecurity", "weather surveillance", "market information"],
40
+ }
41
+
42
+ vis_ref = {
43
+ "very high":["roads", "irrigation"],
44
+ "high":["input subsidies"],
45
+ "medium":["agricultural extension", "credit", "training"],
46
+ "low":["weather surveillance", "market information"],
47
+ "very low":["agricultural research and development"]
48
+ }
49
+
50
+ target_add_ref = {
51
+ "private":{
52
+ "rivalrous":"yes",
53
+ "excludable":"yes"
54
+ },
55
+ "club":{
56
+ "rivalrous":"no",
57
+ "excludable":"yes"
58
+ },
59
+ "quasi public (impure public)":{
60
+ "rivalrous":"no",
61
+ "excludable":"partially"
62
+ },
63
+ "public":{
64
+ "rivalrous":"no",
65
+ "excludable":"no"
66
+ },
67
+ }
68
+
69
+ vis_add_ref = {
70
+ "very high":{
71
+ "desc":"physical infrastructure"
72
+ },
73
+ "high":{
74
+ "desc":"tangible inputs delivered to farmers"
75
+ },
76
+ "medium":{
77
+ "desc":"services or training"
78
+ },
79
+ "low":{
80
+ "desc":"informattional goods"
81
+ },
82
+ "very low":{
83
+ "desc":"knowledge generation, institutional change"
84
+ },
85
+ }
src/peri/investments.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ from src.peri import tti_ref, target_ref, vis_ref, target_add_ref, vis_add_ref
3
+
4
+ def normalize_scores(score_df:pd.DataFrame):
5
+ max_score = max(4, score_df['score'].max())
6
+ min_score = min(1, score_df['score'].min())
7
+ score_df['noremalized_scores'] = (score_df['score']-min_score)/(max_score-min_score)
8
+ return score_df
9
+
10
+ def prepare_investment_codings(var_ref:dict, var_col:str=None):
11
+ score_ref = {k:v for k,v in zip(var_ref, range(len(var_ref), 0, -1))}
12
+ score_df = pd.DataFrame({"investments":[c for v in var_ref.values() for c in v],
13
+ var_col:[k for k,v in var_ref.items() for c in v],
14
+ "score":[score_ref[k] for k,v in var_ref.items() for c in v]})
15
+ return normalize_scores(score_df)
16
+
17
+ def analyze_investments():
18
+ tti_scores = prepare_investment_codings(tti_ref, "time_to_impact")
19
+ target_scores = prepare_investment_codings(target_ref, "type")
20
+ vis_scores = prepare_investment_codings(vis_ref, "observability")
21
+
22
+ merge_cols = ["investments", "score"]
23
+ investment_scores = tti_scores[merge_cols].rename(columns={"score":"time_to_impact"}).merge(target_scores[merge_cols].rename(columns={"score":"targetability"}), on="investments", how="outer")
24
+ investment_df = investment_scores.merge(vis_scores[merge_cols].rename(columns={"score":"visibility"}), on="investments", how="outer")
25
+
26
+ score_cols = ["time_to_impact", "targetability", "visibility"]
27
+ investment_df['agg_score'] = investment_df[score_cols].sum(1)
28
+
29
+ return investment_df
30
+
src/peri/{beneficiaries.py → value_chains.py} RENAMED
@@ -1,13 +1,13 @@
1
  import pandas as pd
2
- from typing import Iterable
3
 
4
- def normalize_scores(score_df:pd.DataFrame):
5
- max_score = max(4, score_df['score'].max())
6
- min_score = min(1, score_df['score'].min())
7
- score_df['noremalized_scores'] = (score_df['score']-min_score)/(max_score-min_score)
8
- return score_df
9
 
10
- def score_beneficiaries(vc_dist_ref:pd.DataFrame,
11
  val_chain_col:str,
12
  ruling_opp_swing_share:Iterable):
13
  vc_dist_ref["score"] = 0
@@ -21,10 +21,25 @@ def score_beneficiaries(vc_dist_ref:pd.DataFrame,
21
  def categorize_vc_production(vc_dist_ref:pd.DataFrame,
22
  val_chain_col:str):
23
  vc_dist_ref = vc_dist_ref[~vc_dist_ref['category'].str.lower().str.contains("plurality")].reset_index(drop=True)
24
- ros_vc = vc_dist_ref.groupby([val_chain_col], as_index=False).apply(lambda x: x[x["share"]>0.5])[val_chain_col].unique()
25
 
26
- score_df = score_beneficiaries(vc_dist_ref, val_chain_col, ros_vc)
27
  score_df = score_df.groupby(['score'], as_index=False)[val_chain_col].unique().explode(val_chain_col).reset_index(drop=True)
28
 
29
  score_df = score_df[[val_chain_col, 'score']]
30
- return normalize_scores(score_df)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import pandas as pd
2
+ from typing import Iterable, Dict
3
 
4
+ from src.llm import generate
5
+ from src.retriever import retrieve
6
+ from src.prompts import build_vc_analyzer_prompt
7
+ from src.query_engine import _parse_vc_analyzer_result
8
+ from .investments import normalize_scores
9
 
10
+ def score_value_chains(vc_dist_ref:pd.DataFrame,
11
  val_chain_col:str,
12
  ruling_opp_swing_share:Iterable):
13
  vc_dist_ref["score"] = 0
 
21
  def categorize_vc_production(vc_dist_ref:pd.DataFrame,
22
  val_chain_col:str):
23
  vc_dist_ref = vc_dist_ref[~vc_dist_ref['category'].str.lower().str.contains("plurality")].reset_index(drop=True)
24
+ ros_vc = vc_dist_ref.groupby([val_chain_col]).apply(lambda x: x[x["share"]>0.5]).reset_index()[val_chain_col].unique()
25
 
26
+ score_df = score_value_chains(vc_dist_ref, val_chain_col, ros_vc)
27
  score_df = score_df.groupby(['score'], as_index=False)[val_chain_col].unique().explode(val_chain_col).reset_index(drop=True)
28
 
29
  score_df = score_df[[val_chain_col, 'score']]
30
+ return normalize_scores(score_df)
31
+
32
+ def analyze_committment_to_vc(cfg:Dict, value_chain:str):
33
+ search_query = f"{value_chain} production in uganda"
34
+ retrieval_result = retrieve(search_query, cfg, route="both")
35
+ context = retrieval_result.get("context", "")
36
+
37
+ prompt = build_vc_analyzer_prompt(context, value_chain, cfg["chatbot"].get("domain"))
38
+ raw = generate(
39
+ "",
40
+ prompt,
41
+ cfg,
42
+ max_tokens=4096,
43
+ )
44
+
45
+ return _parse_vc_analyzer_result(raw)
src/prompts.py CHANGED
@@ -288,16 +288,66 @@ Your task is to review retrieved documents from a knowledge base and extract evi
288
 
289
  IMPORTANT RULES
290
 
291
- 1. Only use information explicitly found in the provided documents.
292
- 2. Do not infer facts that are not supported by evidence.
293
- 3. If evidence is not found, return "NOT_FOUND".
 
 
 
 
294
  4. Every extracted value must be accompanied by:
295
  - supporting quotation
296
  - source document name
297
  - page/section reference when available
298
  5. Multiple documents may be provided.
299
  6. Consolidate information across all documents.
300
- 7. Return output strictly as valid JSON.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
301
 
302
  --------------------------------------------------------
303
  TARGET VALUE CHAIN
@@ -321,12 +371,12 @@ B. Current budget allocation (or in the most recent financial year)
321
 
322
  Extract:
323
 
324
- - budget_start_year
325
- - budget_start_value
326
- - budget_end_year
327
- - budget_end_value
328
- - currency
329
- - responsible institution
330
 
331
  If multiple values are found, report all candidate values and explain differences.
332
 
@@ -425,7 +475,7 @@ OUTPUT FORMAT
425
  --------------------------------------------------------
426
 
427
  {{
428
- "value_chain": "",
429
 
430
  "financial_commitment": {{
431
  "budget_start_year": "",
@@ -485,6 +535,7 @@ OUTPUT FORMAT
485
  "evidence": []
486
  }}
487
  }}
 
488
  }}
489
 
490
  """
 
288
 
289
  IMPORTANT RULES
290
 
291
+ 1. NEVER use your training data or memory to answer questions.
292
+ Every factual claim MUST be grounded in the provided context.
293
+ 2. If the context contains relevant information, you MUST provide
294
+ what you can, even if the answer is incomplete. An incomplete
295
+ answer grounded in sources is ALWAYS better than a refusal.
296
+ 3. ONLY refuse if the context contains NO relevant information at
297
+ all. If evidence is not found, return "NOT_FOUND".
298
  4. Every extracted value must be accompanied by:
299
  - supporting quotation
300
  - source document name
301
  - page/section reference when available
302
  5. Multiple documents may be provided.
303
  6. Consolidate information across all documents.
304
+ 7. STRUCTURED DATA WITH MISSING VALUES: N/A or empty values in
305
+ dataset rows are NORMAL — they mean data was not collected or
306
+ not available for that year/source. Do NOT treat N/A values as
307
+ a reason to refuse answering. Report the data that IS available
308
+ and note which fields have missing values.
309
+ 8. Return output strictly as valid JSON.
310
+
311
+ ---------------------------------------------------------------------
312
+ CITATION RULES
313
+ ---------------------------------------------------------------------
314
+ 1. Every factual claim MUST carry a numbered endnote like [1], [2],
315
+ etc. that maps to a reference at the end of your response.
316
+ Do NOT output the literal text "[N]" — always use actual numbers.
317
+ 2. Direct quote anchoring: when you reproduce three or more
318
+ consecutive words from a source, wrap them in quotation marks
319
+ and attach the endnote number immediately after.
320
+ 3. Paraphrased claims still require a numbered endnote.
321
+
322
+ ---------------------------------------------------------------------
323
+ REFERENCE LIST FORMAT
324
+ ---------------------------------------------------------------------
325
+ At the end of every response, include a "References" section.
326
+
327
+ IMPORTANT: Do NOT use chunk IDs (CHUNK-LOCAL-001, CHUNK-SQL-001, CHUNK-WEB-001, etc.)
328
+ in the reference list. Extract the ACTUAL file name, path, and page
329
+ number from the "From:" and "Path:" lines in the context.
330
+
331
+ For local sources, use the file name and path:
332
+ [1] Chenoweth_Stephan_2011.pdf (p. 23)
333
+ -- knowledge_base/NAVCO 2.0/Chenoweth_Stephan_2011.pdf
334
+
335
+ For web sources, use author, year, title, and URL/DOI:
336
+ [2] Nepstad, S.E. (2011). "Nonviolent Revolutions"
337
+ DOI: https://doi.org/10.xxxx/yyyy
338
+
339
+ Source priority:
340
+ Local sources (user's documents) > Web sources (search results)
341
+
342
+ Organize references into two groups when both are present:
343
+ **Local Sources (Primary)**
344
+ **Web Sources (Supplementary)**
345
+
346
+ ---------------------------------------------------------------------
347
+ RESPONSE LENGTH
348
+ ---------------------------------------------------------------------
349
+ Keep your response length proportional to the available evidence.
350
+ Short context = short answer. Do NOT pad responses.
351
 
352
  --------------------------------------------------------
353
  TARGET VALUE CHAIN
 
371
 
372
  Extract:
373
 
374
+ - budget_start_year (string or datetime)
375
+ - budget_start_value (integer)
376
+ - budget_end_year (string or datetime)
377
+ - budget_end_value (integer)
378
+ - currency (integer)
379
+ - responsible institution (string)
380
 
381
  If multiple values are found, report all candidate values and explain differences.
382
 
 
475
  --------------------------------------------------------
476
 
477
  {{
478
+ {value_chain}:{{
479
 
480
  "financial_commitment": {{
481
  "budget_start_year": "",
 
535
  "evidence": []
536
  }}
537
  }}
538
+ }}
539
  }}
540
 
541
  """
src/query_engine.py CHANGED
@@ -26,7 +26,7 @@ def analyze_committment_to_vc(value_chain:str,
26
  except Exception:
27
  # LLM failure: fall back to raw query
28
  return {
29
- "value_chain": None,
30
 
31
  "financial_commitment": {
32
  "budget_start_year": "",
@@ -86,6 +86,7 @@ def analyze_committment_to_vc(value_chain:str,
86
  "evidence": []
87
  }
88
  }
 
89
  }
90
 
91
 
 
26
  except Exception:
27
  # LLM failure: fall back to raw query
28
  return {
29
+ value_chain:{
30
 
31
  "financial_commitment": {
32
  "budget_start_year": "",
 
86
  "evidence": []
87
  }
88
  }
89
+ }
90
  }
91
 
92
 
src/verifier.py CHANGED
@@ -16,7 +16,7 @@ The main entry point is ``verify_and_respond``.
16
  import json
17
  import re
18
 
19
- from src.prompts import build_prompt, build_verification_prompt
20
  from src.llm import generate
21
  from src.retriever import NO_SOURCES_REFUSAL
22
 
@@ -303,7 +303,7 @@ def parse_verification_result(raw: str) -> dict:
303
  # ── Main entry point ─────────────────────────────────────────────────────────
304
 
305
  def verify_and_respond(
306
- query: str, retrieval_result: dict, cfg: dict,
307
  original_query: str = "",
308
  ) -> dict:
309
  """Generate a response and run it through the 7-layer verification stack.
@@ -346,7 +346,7 @@ def verify_and_respond(
346
  soft_max = compute_soft_max_tokens(len(context), default_max)
347
 
348
  # ── Layer 1: System prompt guardrails (built into prompt) ─────────────
349
- system_prompt = build_prompt(context, bot_name, domain, kb_overview=kb_overview)
350
 
351
  # ── Build user message with original query for intent cross-check ────
352
  if original_query and original_query != query:
 
16
  import json
17
  import re
18
 
19
+ from src.prompts import build_prompt, build_verification_prompt, build_vc_analyzer_prompt
20
  from src.llm import generate
21
  from src.retriever import NO_SOURCES_REFUSAL
22
 
 
303
  # ── Main entry point ─────────────────────────────────────────────────────────
304
 
305
  def verify_and_respond(
306
+ query: str, retrieval_result: dict, cfg: dict, value_chain:str,
307
  original_query: str = "",
308
  ) -> dict:
309
  """Generate a response and run it through the 7-layer verification stack.
 
346
  soft_max = compute_soft_max_tokens(len(context), default_max)
347
 
348
  # ── Layer 1: System prompt guardrails (built into prompt) ─────────────
349
+ system_prompt = build_vc_analyzer_prompt(context, value_chain, domain)
350
 
351
  # ── Build user message with original query for intent cross-check ────
352
  if original_query and original_query != query:
ui.py ADDED
@@ -0,0 +1,276 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #######################
2
+ # Import libraries
3
+ import streamlit as st
4
+ import pandas as pd
5
+ import altair as alt
6
+ import plotly.express as px
7
+
8
+ #######################
9
+ # Page configuration
10
+ st.set_page_config(
11
+ page_title="US Population Dashboard",
12
+ page_icon="🏂",
13
+ layout="wide",
14
+ initial_sidebar_state="expanded")
15
+
16
+ alt.themes.enable("dark")
17
+
18
+ #######################
19
+ # CSS styling
20
+ st.markdown("""
21
+ <style>
22
+
23
+ [data-testid="block-container"] {
24
+ padding-left: 2rem;
25
+ padding-right: 2rem;
26
+ padding-top: 1rem;
27
+ padding-bottom: 0rem;
28
+ margin-bottom: -7rem;
29
+ }
30
+
31
+ [data-testid="stVerticalBlock"] {
32
+ padding-left: 0rem;
33
+ padding-right: 0rem;
34
+ }
35
+
36
+ [data-testid="stMetric"] {
37
+ background-color: #393939;
38
+ text-align: center;
39
+ padding: 15px 0;
40
+ }
41
+
42
+ [data-testid="stMetricLabel"] {
43
+ display: flex;
44
+ justify-content: center;
45
+ align-items: center;
46
+ }
47
+
48
+ [data-testid="stMetricDeltaIcon-Up"] {
49
+ position: relative;
50
+ left: 38%;
51
+ -webkit-transform: translateX(-50%);
52
+ -ms-transform: translateX(-50%);
53
+ transform: translateX(-50%);
54
+ }
55
+
56
+ [data-testid="stMetricDeltaIcon-Down"] {
57
+ position: relative;
58
+ left: 38%;
59
+ -webkit-transform: translateX(-50%);
60
+ -ms-transform: translateX(-50%);
61
+ transform: translateX(-50%);
62
+ }
63
+
64
+ </style>
65
+ """, unsafe_allow_html=True)
66
+
67
+
68
+ #######################
69
+ # Load data
70
+ df_reshaped = pd.read_csv('./us-population-2010-2019-reshaped.csv')
71
+
72
+
73
+ #######################
74
+ # Sidebar
75
+ with st.sidebar:
76
+ st.title('🏂 US Population Dashboard')
77
+
78
+ year_list = list(df_reshaped.year.unique())[::-1]
79
+
80
+ selected_year = st.selectbox('Select a year', year_list)
81
+ df_selected_year = df_reshaped[df_reshaped.year == selected_year]
82
+ df_selected_year_sorted = df_selected_year.sort_values(by="population", ascending=False)
83
+
84
+ color_theme_list = ['blues', 'cividis', 'greens', 'inferno', 'magma', 'plasma', 'reds', 'rainbow', 'turbo', 'viridis']
85
+ selected_color_theme = st.selectbox('Select a color theme', color_theme_list)
86
+
87
+
88
+ #######################
89
+ # Plots
90
+
91
+ # Heatmap
92
+ def make_heatmap(input_df, input_y, input_x, input_color, input_color_theme):
93
+ heatmap = alt.Chart(input_df).mark_rect().encode(
94
+ y=alt.Y(f'{input_y}:O', axis=alt.Axis(title="Year", titleFontSize=18, titlePadding=15, titleFontWeight=900, labelAngle=0)),
95
+ x=alt.X(f'{input_x}:O', axis=alt.Axis(title="", titleFontSize=18, titlePadding=15, titleFontWeight=900)),
96
+ color=alt.Color(f'max({input_color}):Q',
97
+ legend=None,
98
+ scale=alt.Scale(scheme=input_color_theme)),
99
+ stroke=alt.value('black'),
100
+ strokeWidth=alt.value(0.25),
101
+ ).properties(width=900
102
+ ).configure_axis(
103
+ labelFontSize=12,
104
+ titleFontSize=12
105
+ )
106
+ # height=300
107
+ return heatmap
108
+
109
+ # Choropleth map
110
+ def make_choropleth(input_df, input_id, input_column, input_color_theme):
111
+ choropleth = px.choropleth(input_df, locations=input_id, color=input_column, locationmode="USA-states",
112
+ color_continuous_scale=input_color_theme,
113
+ range_color=(0, max(df_selected_year.population)),
114
+ scope="usa",
115
+ labels={'population':'Population'}
116
+ )
117
+ choropleth.update_layout(
118
+ template='plotly_dark',
119
+ plot_bgcolor='rgba(0, 0, 0, 0)',
120
+ paper_bgcolor='rgba(0, 0, 0, 0)',
121
+ margin=dict(l=0, r=0, t=0, b=0),
122
+ height=350
123
+ )
124
+ return choropleth
125
+
126
+
127
+ # Donut chart
128
+ def make_donut(input_response, input_text, input_color):
129
+ if input_color == 'blue':
130
+ chart_color = ['#29b5e8', '#155F7A']
131
+ if input_color == 'green':
132
+ chart_color = ['#27AE60', '#12783D']
133
+ if input_color == 'orange':
134
+ chart_color = ['#F39C12', '#875A12']
135
+ if input_color == 'red':
136
+ chart_color = ['#E74C3C', '#781F16']
137
+
138
+ source = pd.DataFrame({
139
+ "Topic": ['', input_text],
140
+ "% value": [100-input_response, input_response]
141
+ })
142
+ source_bg = pd.DataFrame({
143
+ "Topic": ['', input_text],
144
+ "% value": [100, 0]
145
+ })
146
+
147
+ plot = alt.Chart(source).mark_arc(innerRadius=45, cornerRadius=25).encode(
148
+ theta="% value",
149
+ color= alt.Color("Topic:N",
150
+ scale=alt.Scale(
151
+ #domain=['A', 'B'],
152
+ domain=[input_text, ''],
153
+ # range=['#29b5e8', '#155F7A']), # 31333F
154
+ range=chart_color),
155
+ legend=None),
156
+ ).properties(width=130, height=130)
157
+
158
+ text = plot.mark_text(align='center', color="#29b5e8", font="Lato", fontSize=32, fontWeight=700, fontStyle="italic").encode(text=alt.value(f'{input_response} %'))
159
+ plot_bg = alt.Chart(source_bg).mark_arc(innerRadius=45, cornerRadius=20).encode(
160
+ theta="% value",
161
+ color= alt.Color("Topic:N",
162
+ scale=alt.Scale(
163
+ # domain=['A', 'B'],
164
+ domain=[input_text, ''],
165
+ range=chart_color), # 31333F
166
+ legend=None),
167
+ ).properties(width=130, height=130)
168
+ return plot_bg + plot + text
169
+
170
+ # Convert population to text
171
+ def format_number(num):
172
+ if num > 1000000:
173
+ if not num % 1000000:
174
+ return f'{num // 1000000} M'
175
+ return f'{round(num / 1000000, 1)} M'
176
+ return f'{num // 1000} K'
177
+
178
+ # Calculation year-over-year population migrations
179
+ def calculate_population_difference(input_df, input_year):
180
+ selected_year_data = input_df[input_df['year'] == input_year].reset_index()
181
+ previous_year_data = input_df[input_df['year'] == input_year - 1].reset_index()
182
+ selected_year_data['population_difference'] = selected_year_data.population.sub(previous_year_data.population, fill_value=0)
183
+ return pd.concat([selected_year_data.states, selected_year_data.id, selected_year_data.population, selected_year_data.population_difference], axis=1).sort_values(by="population_difference", ascending=False)
184
+
185
+ def render_db():
186
+ #######################
187
+ # Dashboard Main Panel
188
+ col = st.columns((1.5, 4.5, 2), gap='medium')
189
+
190
+ with col[0]:
191
+ st.markdown('#### Gains/Losses')
192
+
193
+ df_population_difference_sorted = calculate_population_difference(df_reshaped, selected_year)
194
+
195
+ if selected_year > 2010:
196
+ first_state_name = df_population_difference_sorted.states.iloc[0]
197
+ first_state_population = format_number(df_population_difference_sorted.population.iloc[0])
198
+ first_state_delta = format_number(df_population_difference_sorted.population_difference.iloc[0])
199
+ else:
200
+ first_state_name = '-'
201
+ first_state_population = '-'
202
+ first_state_delta = ''
203
+ st.metric(label=first_state_name, value=first_state_population, delta=first_state_delta)
204
+
205
+ if selected_year > 2010:
206
+ last_state_name = df_population_difference_sorted.states.iloc[-1]
207
+ last_state_population = format_number(df_population_difference_sorted.population.iloc[-1])
208
+ last_state_delta = format_number(df_population_difference_sorted.population_difference.iloc[-1])
209
+ else:
210
+ last_state_name = '-'
211
+ last_state_population = '-'
212
+ last_state_delta = ''
213
+ st.metric(label=last_state_name, value=last_state_population, delta=last_state_delta)
214
+
215
+
216
+ st.markdown('#### States Migration')
217
+
218
+ if selected_year > 2010:
219
+ # Filter states with population difference > 50000
220
+ # df_greater_50000 = df_population_difference_sorted[df_population_difference_sorted.population_difference_absolute > 50000]
221
+ df_greater_50000 = df_population_difference_sorted[df_population_difference_sorted.population_difference > 50000]
222
+ df_less_50000 = df_population_difference_sorted[df_population_difference_sorted.population_difference < -50000]
223
+
224
+ # % of States with population difference > 50000
225
+ states_migration_greater = round((len(df_greater_50000)/df_population_difference_sorted.states.nunique())*100)
226
+ states_migration_less = round((len(df_less_50000)/df_population_difference_sorted.states.nunique())*100)
227
+ donut_chart_greater = make_donut(states_migration_greater, 'Inbound Migration', 'green')
228
+ donut_chart_less = make_donut(states_migration_less, 'Outbound Migration', 'red')
229
+ else:
230
+ states_migration_greater = 0
231
+ states_migration_less = 0
232
+ donut_chart_greater = make_donut(states_migration_greater, 'Inbound Migration', 'green')
233
+ donut_chart_less = make_donut(states_migration_less, 'Outbound Migration', 'red')
234
+
235
+ migrations_col = st.columns((0.2, 1, 0.2))
236
+ with migrations_col[1]:
237
+ st.write('Inbound')
238
+ st.altair_chart(donut_chart_greater)
239
+ st.write('Outbound')
240
+ st.altair_chart(donut_chart_less)
241
+
242
+ with col[1]:
243
+ st.markdown('#### Total Population')
244
+
245
+ choropleth = make_choropleth(df_selected_year, 'states_code', 'population', selected_color_theme)
246
+ st.plotly_chart(choropleth, use_container_width=True)
247
+
248
+ heatmap = make_heatmap(df_reshaped, 'year', 'states', 'population', selected_color_theme)
249
+ st.altair_chart(heatmap, use_container_width=True)
250
+
251
+
252
+ with col[2]:
253
+ st.markdown('#### Top States')
254
+
255
+ st.dataframe(df_selected_year_sorted,
256
+ column_order=("states", "population"),
257
+ hide_index=True,
258
+ width='content',
259
+ column_config={
260
+ "states": st.column_config.TextColumn(
261
+ "States",
262
+ ),
263
+ "population": st.column_config.ProgressColumn(
264
+ "Population",
265
+ format="%f",
266
+ min_value=0,
267
+ max_value=max(df_selected_year_sorted.population),
268
+ )}
269
+ )
270
+
271
+ with st.expander('About', expanded=True):
272
+ st.write('''
273
+ - Data: [U.S. Census Bureau](https://www.census.gov/data/datasets/time-series/demo/popest/2010s-state-total.html).
274
+ - :orange[**Gains/Losses**]: states with high inbound/ outbound migration for selected year
275
+ - :orange[**States Migration**]: percentage of states with annual inbound/ outbound migration > 50,000
276
+ ''')