Spaces:
Sleeping
Sleeping
Local updates
Browse files- .gitignore +4 -1
- app_dev.py +242 -0
- app_web.py +247 -5
- src/kb_meta.py +23 -2
- src/peri.py +0 -92
- src/peri/__init__.py +85 -0
- src/peri/investments.py +30 -0
- src/peri/{beneficiaries.py → value_chains.py} +25 -10
- src/prompts.py +62 -11
- src/query_engine.py +2 -1
- src/verifier.py +3 -3
- ui.py +276 -0
.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,
|
| 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 |
-
|
| 562 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 263 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
|
| 10 |
-
def
|
| 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]
|
| 25 |
|
| 26 |
-
score_df =
|
| 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.
|
| 292 |
-
|
| 293 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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 |
-
|
| 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 =
|
| 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 |
+
''')
|