Spaces:
Sleeping
Sleeping
cassiebuhler
commited on
Commit
Β·
2d92cf6
1
Parent(s):
02885ef
reviving app
Browse files- app/app.py +116 -43
- app/utils.py +111 -9
- app/variables.py +108 -5
- requirements.txt +2 -2
app/app.py
CHANGED
|
@@ -16,21 +16,20 @@ A data visualization tool built for the Trust for Public Land
|
|
| 16 |
'''
|
| 17 |
|
| 18 |
pmtiles = get_pmtiles_url() # generate PMTiles url
|
| 19 |
-
|
| 20 |
with st.sidebar:
|
| 21 |
leafmap_choice = st.selectbox("Leafmap module", ['maplibregl','foliumap'])
|
| 22 |
|
| 23 |
if leafmap_choice == "maplibregl":
|
| 24 |
leafmap = importlib.import_module("leafmap.maplibregl")
|
| 25 |
-
m = leafmap.Map(style="positron")
|
|
|
|
| 26 |
else:
|
| 27 |
leafmap = importlib.import_module("leafmap.foliumap")
|
| 28 |
m = leafmap.Map(center=[35, -100], zoom=5, layers_control=True, fullscreen_control=True)
|
| 29 |
|
| 30 |
-
basemaps = leafmap.basemaps.keys()
|
| 31 |
|
| 32 |
with st.sidebar:
|
| 33 |
-
b = st.selectbox("Basemap", basemaps)
|
| 34 |
m.add_basemap(b)
|
| 35 |
st.divider()
|
| 36 |
|
|
@@ -52,6 +51,7 @@ with st.sidebar:
|
|
| 52 |
county_choice = 'All'
|
| 53 |
st.divider()
|
| 54 |
|
|
|
|
| 55 |
# get all the ids that correspond to the filter
|
| 56 |
gdf = filter_data(tpl_table, state_choice, county_choice, year_range)
|
| 57 |
gdf_landvote = filter_data(landvote_table, state_choice, county_choice, year_range)
|
|
@@ -101,45 +101,36 @@ prompt = ChatPromptTemplate.from_messages([
|
|
| 101 |
structured_llm = llm.with_structured_output(SQLResponse)
|
| 102 |
few_shot_structured_llm = prompt | structured_llm
|
| 103 |
|
| 104 |
-
|
| 105 |
-
def run_sql(query,
|
| 106 |
"""
|
| 107 |
Filter data based on an LLM-generated SQL query and return matching IDs.
|
| 108 |
-
|
| 109 |
-
Args:
|
| 110 |
-
query (str): The natural language query to filter the data.
|
| 111 |
-
color_choice (str): The column used for plotting.
|
| 112 |
"""
|
| 113 |
output = few_shot_structured_llm.invoke(query)
|
| 114 |
sql_query = output.sql_query
|
| 115 |
explanation =output.explanation
|
| 116 |
if not sql_query: # if the chatbot can't generate a SQL query.
|
| 117 |
-
|
| 118 |
-
return pd.DataFrame({'fid' : []})
|
| 119 |
result = con.sql(sql_query).distinct().execute()
|
| 120 |
-
if result.empty
|
| 121 |
explanation = "This query did not return any results. Please try again with a different query."
|
| 122 |
-
st.warning(explanation, icon="β οΈ")
|
| 123 |
-
st.caption("SQL Query:")
|
| 124 |
-
st.code(sql_query,language = "sql")
|
| 125 |
if 'geom' in result.columns:
|
| 126 |
-
return result.drop('geom',axis = 1)
|
| 127 |
else:
|
| 128 |
-
return result
|
| 129 |
-
|
| 130 |
-
|
| 131 |
|
| 132 |
-
with st.popover("Explanation"):
|
| 133 |
-
st.write(explanation)
|
| 134 |
-
st.caption("SQL Query:")
|
| 135 |
-
st.code(sql_query,language = "sql")
|
| 136 |
-
return result
|
| 137 |
|
| 138 |
with chatbot_container:
|
| 139 |
with llm_left_col:
|
| 140 |
example_query = "π Input query here"
|
| 141 |
prompt = st.chat_input(example_query, key="chain", max_chars=300)
|
| 142 |
-
|
|
|
|
|
|
|
|
|
|
| 143 |
# new container for output so it doesn't mess with the alignment of llm options
|
| 144 |
with st.container():
|
| 145 |
if prompt:
|
|
@@ -147,19 +138,60 @@ with st.container():
|
|
| 147 |
try:
|
| 148 |
with st.chat_message("assistant"):
|
| 149 |
with st.spinner("Invoking query..."):
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
else:
|
| 158 |
-
ids = []
|
|
|
|
|
|
|
| 159 |
except Exception as e:
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
st.stop()
|
| 164 |
##### end of chatbot code
|
| 165 |
|
|
@@ -175,19 +207,60 @@ if 'style' not in locals():
|
|
| 175 |
|
| 176 |
# add pmtiles to map (using user-specified module)
|
| 177 |
if leafmap_choice == "maplibregl":
|
| 178 |
-
m.add_pmtiles(pmtiles, style=style, tooltip=True, fit_bounds=True)
|
| 179 |
m.fit_bounds(bounds)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
else:
|
| 181 |
-
m.add_pmtiles(pmtiles, style
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
m.zoom_to_bounds(bounds)
|
|
|
|
| 183 |
|
| 184 |
m.to_streamlit()
|
| 185 |
-
|
| 186 |
with st.expander("π View/download data"): # adding data table
|
| 187 |
-
if '
|
| 188 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
else:
|
| 190 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 191 |
|
| 192 |
public_dollars, private_dollars, total_dollars = tpl_summary(gdf)
|
| 193 |
|
|
|
|
| 16 |
'''
|
| 17 |
|
| 18 |
pmtiles = get_pmtiles_url() # generate PMTiles url
|
|
|
|
| 19 |
with st.sidebar:
|
| 20 |
leafmap_choice = st.selectbox("Leafmap module", ['maplibregl','foliumap'])
|
| 21 |
|
| 22 |
if leafmap_choice == "maplibregl":
|
| 23 |
leafmap = importlib.import_module("leafmap.maplibregl")
|
| 24 |
+
m = leafmap.Map(style="positron", use_message_queue=True)
|
| 25 |
+
|
| 26 |
else:
|
| 27 |
leafmap = importlib.import_module("leafmap.foliumap")
|
| 28 |
m = leafmap.Map(center=[35, -100], zoom=5, layers_control=True, fullscreen_control=True)
|
| 29 |
|
|
|
|
| 30 |
|
| 31 |
with st.sidebar:
|
| 32 |
+
b = st.selectbox("Basemap", basemaps, index= 3)
|
| 33 |
m.add_basemap(b)
|
| 34 |
st.divider()
|
| 35 |
|
|
|
|
| 51 |
county_choice = 'All'
|
| 52 |
st.divider()
|
| 53 |
|
| 54 |
+
legend, position, bg_color, fontsize, shape_type, controls = get_legend(paint, leafmap_choice)
|
| 55 |
# get all the ids that correspond to the filter
|
| 56 |
gdf = filter_data(tpl_table, state_choice, county_choice, year_range)
|
| 57 |
gdf_landvote = filter_data(landvote_table, state_choice, county_choice, year_range)
|
|
|
|
| 101 |
structured_llm = llm.with_structured_output(SQLResponse)
|
| 102 |
few_shot_structured_llm = prompt | structured_llm
|
| 103 |
|
| 104 |
+
@st.cache_data(show_spinner = False)
|
| 105 |
+
def run_sql(query, llm_choice):
|
| 106 |
"""
|
| 107 |
Filter data based on an LLM-generated SQL query and return matching IDs.
|
| 108 |
+
Args: query (str): The natural language query to filter the data.
|
|
|
|
|
|
|
|
|
|
| 109 |
"""
|
| 110 |
output = few_shot_structured_llm.invoke(query)
|
| 111 |
sql_query = output.sql_query
|
| 112 |
explanation =output.explanation
|
| 113 |
if not sql_query: # if the chatbot can't generate a SQL query.
|
| 114 |
+
return pd.DataFrame({'fid' : []}),'', explanation
|
|
|
|
| 115 |
result = con.sql(sql_query).distinct().execute()
|
| 116 |
+
if result.empty:
|
| 117 |
explanation = "This query did not return any results. Please try again with a different query."
|
|
|
|
|
|
|
|
|
|
| 118 |
if 'geom' in result.columns:
|
| 119 |
+
return result.drop('geom',axis = 1), sql_query, explanation
|
| 120 |
else:
|
| 121 |
+
return result, sql_query, explanation
|
| 122 |
+
return result, sql_query, explanation
|
| 123 |
+
|
| 124 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
|
| 126 |
with chatbot_container:
|
| 127 |
with llm_left_col:
|
| 128 |
example_query = "π Input query here"
|
| 129 |
prompt = st.chat_input(example_query, key="chain", max_chars=300)
|
| 130 |
+
_,log_query_col, _ = st.columns([.001, 5,1], vertical_alignment = "top")
|
| 131 |
+
with log_query_col:
|
| 132 |
+
log_queries = st.checkbox("Save query", value = True, help = "Saving your queries helps improve this tool and guide conservation efforts. Your data is stored in a private location. For more details, see 'Why save your queries?' at the bottom of this page.")
|
| 133 |
+
|
| 134 |
# new container for output so it doesn't mess with the alignment of llm options
|
| 135 |
with st.container():
|
| 136 |
if prompt:
|
|
|
|
| 138 |
try:
|
| 139 |
with st.chat_message("assistant"):
|
| 140 |
with st.spinner("Invoking query..."):
|
| 141 |
+
llm_output, sql_query, llm_explanation = run_sql(prompt, llm_choice)
|
| 142 |
+
minio_logger(log_queries, prompt, sql_query, llm_explanation, llm_choice, 'query_log.csv', "shared-tpl")
|
| 143 |
+
# no sql query generated by chatbot
|
| 144 |
+
if sql_query == '':
|
| 145 |
+
st.success(llm_explanation)
|
| 146 |
+
not_mapping = True
|
| 147 |
+
else:
|
| 148 |
+
# sql query generated but no results returned
|
| 149 |
+
if llm_output.empty:
|
| 150 |
+
st.warning(llm_explanation, icon="β οΈ")
|
| 151 |
+
st.caption("SQL Query:")
|
| 152 |
+
st.code(sql_query, language="sql")
|
| 153 |
+
st.stop()
|
| 154 |
+
|
| 155 |
+
# output without mapping columns (id, geom)
|
| 156 |
+
elif "fid" not in llm_output.columns and "geom" not in llm_output.columns:
|
| 157 |
+
st.write(llm_output)
|
| 158 |
+
not_mapping = True
|
| 159 |
+
|
| 160 |
+
# print results
|
| 161 |
+
with st.popover("Explanation"):
|
| 162 |
+
st.write(llm_explanation)
|
| 163 |
+
if sql_query != '':
|
| 164 |
+
st.caption("SQL Query:")
|
| 165 |
+
st.code(sql_query,language = "sql")
|
| 166 |
+
|
| 167 |
+
# extract ids, columns, bounds if present
|
| 168 |
+
if "fid" in llm_output.columns and not llm_output.empty:
|
| 169 |
+
ids = list(set(llm_output['fid'].tolist()))
|
| 170 |
+
llm_cols = extract_columns(sql_query)
|
| 171 |
+
bounds = llm_output.total_bounds.tolist()
|
| 172 |
else:
|
| 173 |
+
ids, llm_cols = [], []
|
| 174 |
+
not_mapping = True
|
| 175 |
+
|
| 176 |
except Exception as e:
|
| 177 |
+
tb_str = traceback.format_exc() # full multiline traceback string
|
| 178 |
+
if isinstance(e, openai.BadRequestError):
|
| 179 |
+
st.error(error_messages["bad_request"](llm_choice, e, tb_str), icon="π¨")
|
| 180 |
+
|
| 181 |
+
elif isinstance(e, openai.RateLimitError):
|
| 182 |
+
st.error(error_messages["bad_request"](llm_choice, e, tb_str), icon="π¨")
|
| 183 |
+
|
| 184 |
+
elif isinstance(e, openai.APIStatusError):
|
| 185 |
+
st.error(error_messages["bad_request"](llm_choice, e, tb_str), icon="π¨")
|
| 186 |
+
|
| 187 |
+
elif isinstance(e, openai.InternalServerError):
|
| 188 |
+
st.error(error_messages["internal_server_error"](llm_choice, e, tb_str), icon="π¨")
|
| 189 |
+
|
| 190 |
+
elif isinstance(e, openai.NotFoundError):
|
| 191 |
+
st.error(error_messages["internal_server_error"](llm_choice, e, tb_str), icon="π¨")
|
| 192 |
+
else:
|
| 193 |
+
prompt = prompt.replace('\n', '')
|
| 194 |
+
st.error(error_messages["unexpected_llm_error"](prompt, e, tb_str))
|
| 195 |
st.stop()
|
| 196 |
##### end of chatbot code
|
| 197 |
|
|
|
|
| 207 |
|
| 208 |
# add pmtiles to map (using user-specified module)
|
| 209 |
if leafmap_choice == "maplibregl":
|
|
|
|
| 210 |
m.fit_bounds(bounds)
|
| 211 |
+
m.add_pmtiles(pmtiles, style=style,
|
| 212 |
+
name="Conservation Almanac",
|
| 213 |
+
attribution = "Trust for Public Land (2025)", tooltip=True,
|
| 214 |
+
template = tooltip_template)
|
| 215 |
+
m.fit_bounds(bounds)
|
| 216 |
+
if style_choice == "Acquisition Cost":
|
| 217 |
+
colors, vmin, vmax, orientation, position, label, height, width = get_colorbar(gdf,paint)
|
| 218 |
+
m.add_colorbar(palette = colors, vmin=vmin, vmax=vmax,
|
| 219 |
+
orientation = orientation, position = position, transparent = True,
|
| 220 |
+
height = height, width = width)
|
| 221 |
+
|
| 222 |
+
else:
|
| 223 |
+
m.add_legend(title = '', legend_dict = legend, fontsize = fontsize,
|
| 224 |
+
bg_color = bg_color, position = position,
|
| 225 |
+
shape_type = shape_type)
|
| 226 |
else:
|
| 227 |
+
m.add_pmtiles(pmtiles, style=style,
|
| 228 |
+
name="Conservation Almanac by Trust for Public Land",
|
| 229 |
+
tooltip=False, zoom_to_layer=True)
|
| 230 |
+
# add custom tooltip to pmtiles layer
|
| 231 |
+
for layer in m._children.values():
|
| 232 |
+
if isinstance(layer, leafmap.PMTilesLayer):
|
| 233 |
+
pmtiles_layer = layer
|
| 234 |
+
break
|
| 235 |
+
|
| 236 |
+
pmtiles_layer.add_child(CustomTooltip())
|
| 237 |
+
style = {'background-color': 'rgba(255, 255, 255, 1)'}
|
| 238 |
+
m.add_legend(title = '', legend_dict = legend, style = style,
|
| 239 |
+
position = position, shape_type = shape_type, draggable = False)
|
| 240 |
m.zoom_to_bounds(bounds)
|
| 241 |
+
|
| 242 |
|
| 243 |
m.to_streamlit()
|
| 244 |
+
|
| 245 |
with st.expander("π View/download data"): # adding data table
|
| 246 |
+
if 'llm_output' not in locals():
|
| 247 |
+
# cols = ['tpl_id','municipality','date','geom']
|
| 248 |
+
group_cols = ['sponsor','program','year']
|
| 249 |
+
gdf_grouped = (gdf.head(100).execute().groupby(group_cols)
|
| 250 |
+
.agg({col: ('sum' if col in ['acres','amount'] else 'first')
|
| 251 |
+
for col in gdf.columns if col not in group_cols})).reset_index()
|
| 252 |
+
cols = ['fid', 'state', 'county','site', 'acres', 'year', 'amount', 'owner', 'owner_type',
|
| 253 |
+
'manager', 'manager_type', 'purchase_type', 'easement', 'easement_type', 'access_type',
|
| 254 |
+
'purpose_type', 'duration_type', 'data_provider', 'data_source', 'source_date',
|
| 255 |
+
'data_aggregator', 'comments', 'program_id', 'program', 'sponsor_id', 'sponsor', 'sponsor_type']
|
| 256 |
+
|
| 257 |
+
st.dataframe(gdf_grouped[cols], use_container_width = True)
|
| 258 |
else:
|
| 259 |
+
if ('geom' in llm_output.columns) and (not llm_output.empty):
|
| 260 |
+
llm_output = llm_output.drop('geom',axis = 1)
|
| 261 |
+
st.dataframe(llm_output, use_container_width = True)
|
| 262 |
+
|
| 263 |
+
|
| 264 |
|
| 265 |
public_dollars, private_dollars, total_dollars = tpl_summary(gdf)
|
| 266 |
|
app/utils.py
CHANGED
|
@@ -3,6 +3,8 @@ from ibis import _
|
|
| 3 |
from variables import *
|
| 4 |
import altair as alt
|
| 5 |
import re
|
|
|
|
|
|
|
| 6 |
|
| 7 |
def get_pmtiles_url():
|
| 8 |
return client.get_presigned_url(
|
|
@@ -13,8 +15,6 @@ def get_pmtiles_url():
|
|
| 13 |
)
|
| 14 |
|
| 15 |
def get_counties(state_selection):
|
| 16 |
-
tpl_table.head().execute()
|
| 17 |
-
|
| 18 |
if state_selection != 'All':
|
| 19 |
counties = tpl_table.filter(_.state == state_selection).select('county').distinct().order_by('county').execute()
|
| 20 |
counties = ['All'] + counties['county'].tolist()
|
|
@@ -120,15 +120,45 @@ def tpl_style(ids, paint, pmtiles):
|
|
| 120 |
}
|
| 121 |
return style
|
| 122 |
|
| 123 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
"""
|
| 125 |
Generates a legend dictionary with color mapping and formatting adjustments.
|
| 126 |
"""
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
|
| 133 |
@st.cache_data
|
| 134 |
def tpl_summary(_df):
|
|
@@ -178,4 +208,76 @@ def chart_time(timeseries, column, paint):
|
|
| 178 |
y = alt.Y('amount:Q'),
|
| 179 |
color=alt.Color(column,scale= alt.Scale(domain=domain, range=range_))
|
| 180 |
).properties(height=350)
|
| 181 |
-
return plt
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
from variables import *
|
| 4 |
import altair as alt
|
| 5 |
import re
|
| 6 |
+
from leafmap.foliumap import PMTilesMapLibreTooltip
|
| 7 |
+
from branca.element import Template
|
| 8 |
|
| 9 |
def get_pmtiles_url():
|
| 10 |
return client.get_presigned_url(
|
|
|
|
| 15 |
)
|
| 16 |
|
| 17 |
def get_counties(state_selection):
|
|
|
|
|
|
|
| 18 |
if state_selection != 'All':
|
| 19 |
counties = tpl_table.filter(_.state == state_selection).select('county').distinct().order_by('county').execute()
|
| 20 |
counties = ['All'] + counties['county'].tolist()
|
|
|
|
| 120 |
}
|
| 121 |
return style
|
| 122 |
|
| 123 |
+
def get_colorbar(gdf, paint):
|
| 124 |
+
"""
|
| 125 |
+
Extracts color hex codes and value range (vmin, vmax) from paint
|
| 126 |
+
to make a color bar. Used for mapping continuous data.
|
| 127 |
+
"""
|
| 128 |
+
# numbers = [x for x in paint if isinstance(x, (int, float))]
|
| 129 |
+
vmin = gdf.amount.min().execute()
|
| 130 |
+
vmax = gdf.amount.max().execute()
|
| 131 |
+
# min(numbers), max(numbers),
|
| 132 |
+
colors = [x for x in paint if isinstance(x, str) and x.startswith('#')]
|
| 133 |
+
orientation = 'vertical'
|
| 134 |
+
position = 'bottom-right'
|
| 135 |
+
label = "Acquisition Cost"
|
| 136 |
+
height = 3
|
| 137 |
+
width = .2
|
| 138 |
+
return colors, vmin, vmax, orientation, position, label, height, width
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def get_legend(paint, leafmap_backend, df = None, column = None):
|
| 142 |
"""
|
| 143 |
Generates a legend dictionary with color mapping and formatting adjustments.
|
| 144 |
"""
|
| 145 |
+
if 'stops' in paint:
|
| 146 |
+
legend = {cat: color for cat, color in paint['stops']}
|
| 147 |
+
else:
|
| 148 |
+
legend = {}
|
| 149 |
+
if df is not None:
|
| 150 |
+
if ~df.empty:
|
| 151 |
+
categories = df[column].to_list() #if we filter out categories, don't show them on the legend
|
| 152 |
+
legend = {cat: color for cat, color in legend.items() if str(cat) in categories}
|
| 153 |
+
position, fontsize, bg_color = 'bottomright', 15, 'white'
|
| 154 |
+
controls={'navigation': 'bottom-left',
|
| 155 |
+
'fullscreen':'bottom-left'}
|
| 156 |
+
shape_type = 'circle'
|
| 157 |
+
|
| 158 |
+
if leafmap_backend == 'maplibregl':
|
| 159 |
+
position = 'bottom-right'
|
| 160 |
+
return legend, position, bg_color, fontsize, shape_type, controls
|
| 161 |
+
|
| 162 |
|
| 163 |
@st.cache_data
|
| 164 |
def tpl_summary(_df):
|
|
|
|
| 208 |
y = alt.Y('amount:Q'),
|
| 209 |
color=alt.Color(column,scale= alt.Scale(domain=domain, range=range_))
|
| 210 |
).properties(height=350)
|
| 211 |
+
return plt
|
| 212 |
+
|
| 213 |
+
class CustomTooltip(PMTilesMapLibreTooltip):
|
| 214 |
+
_template = Template("""
|
| 215 |
+
{% macro script(this, kwargs) -%}
|
| 216 |
+
var maplibre = {{ this._parent.get_name() }}.getMaplibreMap();
|
| 217 |
+
const popup = new maplibregl.Popup({ closeButton: false, closeOnClick: false });
|
| 218 |
+
|
| 219 |
+
maplibre.on('mousemove', function(e) {
|
| 220 |
+
const features = maplibre.queryRenderedFeatures(e.point);
|
| 221 |
+
const filtered = features.filter(f => f.properties && f.properties.fid);
|
| 222 |
+
|
| 223 |
+
if (filtered.length) {
|
| 224 |
+
const props = filtered[0].properties;
|
| 225 |
+
const html = `
|
| 226 |
+
<div><strong>fid:</strong> ${props.fid || 'N/A'}</div>
|
| 227 |
+
<div><strong>Site:</strong> ${props.site || 'N/A'}</div>
|
| 228 |
+
<div><strong>Sponsor:</strong> ${props.sponsor || 'N/A'}</div>
|
| 229 |
+
<div><strong>Program:</strong> ${props.program || 'N/A'}</div>
|
| 230 |
+
<div><strong>State:</strong> ${props.state || 'N/A'}</div>
|
| 231 |
+
<div><strong>County:</strong> ${props.county || 'N/A'}</div>
|
| 232 |
+
<div><strong>Year:</strong> ${props.year || 'N/A'}</div>
|
| 233 |
+
<div><strong>Manager:</strong> ${props.manager || 'N/A'}</div>
|
| 234 |
+
<div>
|
| 235 |
+
<strong>Amount:</strong> ${
|
| 236 |
+
props.amount
|
| 237 |
+
? `$${parseFloat(props.amount).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
|
| 238 |
+
: 'N/A'
|
| 239 |
+
}
|
| 240 |
+
</div>
|
| 241 |
+
<div>
|
| 242 |
+
<strong>Acres:</strong> ${
|
| 243 |
+
props.acres
|
| 244 |
+
? parseFloat(props.acres).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
| 245 |
+
: 'N/A'
|
| 246 |
+
}
|
| 247 |
+
</div>
|
| 248 |
+
`;
|
| 249 |
+
popup.setLngLat(e.lngLat).setHTML(html).addTo(maplibre);
|
| 250 |
+
if (popup._container) {
|
| 251 |
+
popup._container.style.zIndex = 9999;
|
| 252 |
+
}
|
| 253 |
+
} else {
|
| 254 |
+
popup.remove();
|
| 255 |
+
}
|
| 256 |
+
});
|
| 257 |
+
{% endmacro %}
|
| 258 |
+
""")
|
| 259 |
+
|
| 260 |
+
minio_key = os.getenv("MINIO_KEY")
|
| 261 |
+
if minio_key is None:
|
| 262 |
+
minio_key = st.secrets["MINIO_KEY"]
|
| 263 |
+
|
| 264 |
+
minio_secret = os.getenv("MINIO_SECRET")
|
| 265 |
+
if minio_secret is None:
|
| 266 |
+
minio_secret = st.secrets["MINIO_SECRET"]
|
| 267 |
+
|
| 268 |
+
def minio_logger(consent, query, sql_query, llm_explanation, llm_choice, filename="query_log.csv", bucket="shared-tpl",
|
| 269 |
+
key=minio_key, secret=minio_secret,
|
| 270 |
+
endpoint="minio.carlboettiger.info"):
|
| 271 |
+
mc = minio.Minio(endpoint, key, secret)
|
| 272 |
+
mc.fget_object(bucket, filename, filename)
|
| 273 |
+
log = pd.read_csv(filename)
|
| 274 |
+
timestamp = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
|
| 275 |
+
if consent:
|
| 276 |
+
df = pd.DataFrame({"timestamp": [timestamp], "user_query": [query], "llm_sql": [sql_query], "llm_explanation": [llm_explanation], "llm_choice":[llm_choice]})
|
| 277 |
+
|
| 278 |
+
# if user opted out, do not store query
|
| 279 |
+
else:
|
| 280 |
+
df = pd.DataFrame({"timestamp": [timestamp], "user_query": ['USER OPTED OUT'], "llm_sql": [''], "llm_explanation": [''], "llm_choice":['']})
|
| 281 |
+
|
| 282 |
+
pd.concat([log,df]).to_csv(filename, index=False, header=True)
|
| 283 |
+
mc.fput_object(bucket, filename, filename, content_type="text/csv")
|
app/variables.py
CHANGED
|
@@ -60,6 +60,7 @@ style_options = {
|
|
| 60 |
"Acquisition Cost":
|
| 61 |
["interpolate",
|
| 62 |
['exponential', 1],
|
|
|
|
| 63 |
["get", "amount"],
|
| 64 |
0, "#fde725",
|
| 65 |
36000, "#b4de2c",
|
|
@@ -122,6 +123,106 @@ style_choice_columns = {'Manager Type': style_options['Manager Type']['property'
|
|
| 122 |
'Measure Cost': 'conservation_funds_approved',
|
| 123 |
}
|
| 124 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
from langchain_openai import ChatOpenAI
|
| 126 |
import streamlit as st
|
| 127 |
from langchain_openai.chat_models.base import BaseChatOpenAI
|
|
@@ -143,9 +244,11 @@ llm_options = {
|
|
| 143 |
"deepseek-r1t2-chimera": ChatOpenAI(model = "tngtech/deepseek-r1t2-chimera:free", api_key=openrouter_api, base_url = "https://openrouter.ai/api/v1", temperature=0),
|
| 144 |
"kimi-dev-72b": ChatOpenAI(model = "moonshotai/kimi-dev-72b:free", api_key=openrouter_api, base_url = "https://openrouter.ai/api/v1", temperature=0),
|
| 145 |
"hunyuan-a13b-instruct": ChatOpenAI(model = "tencent/hunyuan-a13b-instruct:free", api_key=openrouter_api, base_url = "https://openrouter.ai/api/v1", temperature=0),
|
| 146 |
-
"
|
| 147 |
-
"
|
| 148 |
-
"
|
| 149 |
-
"
|
|
|
|
|
|
|
| 150 |
|
| 151 |
-
}
|
|
|
|
| 60 |
"Acquisition Cost":
|
| 61 |
["interpolate",
|
| 62 |
['exponential', 1],
|
| 63 |
+
# ['linear'],
|
| 64 |
["get", "amount"],
|
| 65 |
0, "#fde725",
|
| 66 |
36000, "#b4de2c",
|
|
|
|
| 123 |
'Measure Cost': 'conservation_funds_approved',
|
| 124 |
}
|
| 125 |
|
| 126 |
+
basemaps = ['CartoDB.DarkMatter', 'CartoDB.DarkMatterNoLabels',
|
| 127 |
+
'CartoDB.DarkMatterOnlyLabels','CartoDB.Positron',
|
| 128 |
+
'CartoDB.PositronNoLabels', 'CartoDB.PositronOnlyLabels',
|
| 129 |
+
'CartoDB.Voyager', 'CartoDB.VoyagerLabelsUnder', 'CartoDB.VoyagerNoLabels',
|
| 130 |
+
'CartoDB.VoyagerOnlyLabels', 'CyclOSM', 'Esri.NatGeoWorldMap',
|
| 131 |
+
'Esri.WorldGrayCanvas', 'Esri.WorldPhysical', 'Esri.WorldShadedRelief',
|
| 132 |
+
'Esri.WorldStreetMap', 'Gaode.Normal', 'Gaode.Satellite',
|
| 133 |
+
'NASAGIBS.ASTER_GDEM_Greyscale_Shaded_Relief', 'NASAGIBS.BlueMarble',
|
| 134 |
+
'NASAGIBS.ModisTerraBands367CR','NASAGIBS.ModisTerraTrueColorCR',
|
| 135 |
+
'NLCD 2021 CONUS Land Cover', 'OPNVKarte',
|
| 136 |
+
'OpenStreetMap', 'OpenTopoMap', 'SafeCast',
|
| 137 |
+
'TopPlusOpen.Color', 'TopPlusOpen.Grey', 'UN.ClearMap',
|
| 138 |
+
'USGS Hydrography', 'USGS.USImagery', 'USGS.USImageryTopo',
|
| 139 |
+
'USGS.USTopo']
|
| 140 |
+
|
| 141 |
+
# legend_labels = {
|
| 142 |
+
# "Acquisition Cost": [],
|
| 143 |
+
# "Manager Type":
|
| 144 |
+
# ['Federal', 'State', 'Local','District','Unknown','Joint','Tribal','Private', 'NGO'],
|
| 145 |
+
# "Access":
|
| 146 |
+
# ['Open Access', 'Closed','Unknown','Restricted'],
|
| 147 |
+
# "Purpose":
|
| 148 |
+
# ['Forestry','Historical','Unknown','Other','Farming','Recreation','Environment','Scenic','RAN'],
|
| 149 |
+
# }
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
#maplibregl tooltip
|
| 153 |
+
tooltip_cols = ['fid','state','site','sponsor','program','county','year','manager',
|
| 154 |
+
'amount','acres']
|
| 155 |
+
tooltip_template = "<br>".join([f"{col}: {{{{ {col} }}}}" for col in tooltip_cols])
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
error_messages = {
|
| 159 |
+
"bad_request": lambda llm, e, tb_str: f"""
|
| 160 |
+
**Error β LLM Unavailable**
|
| 161 |
+
|
| 162 |
+
*The LLM you selected `{llm}` is no longer available. Please select a different model.*
|
| 163 |
+
|
| 164 |
+
**Error Details:**
|
| 165 |
+
`{type(e)}: {e}`
|
| 166 |
+
|
| 167 |
+
""",
|
| 168 |
+
|
| 169 |
+
"internal_server_error": lambda llm, e, tb_str: f"""
|
| 170 |
+
**Error β LLM Temporarily Unavailable**
|
| 171 |
+
|
| 172 |
+
The LLM you selected `{llm}` is currently down due to maintenance or provider outages. It may remain offline for several hours.
|
| 173 |
+
|
| 174 |
+
**Please select a different model or try again later.**
|
| 175 |
+
|
| 176 |
+
**Error Details:**
|
| 177 |
+
`{type(e)}: {e}`
|
| 178 |
+
|
| 179 |
+
""",
|
| 180 |
+
|
| 181 |
+
"unexpected_llm_error": lambda prompt, e, tb_str: f"""
|
| 182 |
+
π **BUG: Unexpected Error in Application**
|
| 183 |
+
|
| 184 |
+
An error occurred while processing your query:
|
| 185 |
+
|
| 186 |
+
> "{prompt}"
|
| 187 |
+
|
| 188 |
+
**Error Details:**
|
| 189 |
+
`{type(e)}: {e}`
|
| 190 |
+
|
| 191 |
+
Traceback:
|
| 192 |
+
|
| 193 |
+
```{tb_str}```
|
| 194 |
+
---
|
| 195 |
+
|
| 196 |
+
π¨ **Help Us Improve!**
|
| 197 |
+
|
| 198 |
+
Please help us fix this issue by reporting it on GitHub:
|
| 199 |
+
[π Report this issue](https://github.com/boettiger-lab/CBN-taskforce/issues)
|
| 200 |
+
|
| 201 |
+
Include the query you ran and any other relevant details. Thanks!
|
| 202 |
+
""",
|
| 203 |
+
|
| 204 |
+
"unexpected_error": lambda e, tb_str: f"""
|
| 205 |
+
π **BUG: Unexpected Error in Application**
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
**Error Details:**
|
| 209 |
+
`{type(e)}: {e}`
|
| 210 |
+
|
| 211 |
+
Traceback:
|
| 212 |
+
|
| 213 |
+
```{tb_str}```
|
| 214 |
+
|
| 215 |
+
---
|
| 216 |
+
|
| 217 |
+
π¨ **Help Us Improve!**
|
| 218 |
+
|
| 219 |
+
Please help us fix this issue by reporting it on GitHub:
|
| 220 |
+
[π Report this issue](https://github.com/boettiger-lab/CBN-taskforce/issues)
|
| 221 |
+
|
| 222 |
+
Include the steps you took to get this message and any other details that might help us debug. Thanks!
|
| 223 |
+
"""
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
from langchain_openai import ChatOpenAI
|
| 227 |
import streamlit as st
|
| 228 |
from langchain_openai.chat_models.base import BaseChatOpenAI
|
|
|
|
| 244 |
"deepseek-r1t2-chimera": ChatOpenAI(model = "tngtech/deepseek-r1t2-chimera:free", api_key=openrouter_api, base_url = "https://openrouter.ai/api/v1", temperature=0),
|
| 245 |
"kimi-dev-72b": ChatOpenAI(model = "moonshotai/kimi-dev-72b:free", api_key=openrouter_api, base_url = "https://openrouter.ai/api/v1", temperature=0),
|
| 246 |
"hunyuan-a13b-instruct": ChatOpenAI(model = "tencent/hunyuan-a13b-instruct:free", api_key=openrouter_api, base_url = "https://openrouter.ai/api/v1", temperature=0),
|
| 247 |
+
# "deepseek-chat-v3-0324": ChatOpenAI(model = "deepseek/deepseek-chat-v3-0324:free", api_key=openrouter_api, base_url = "https://openrouter.ai/api/v1", temperature=0),
|
| 248 |
+
"olmo": ChatOpenAI(model = "olmo", api_key=api_key, base_url = "https://llm.nrp-nautilus.io/", temperature=0),
|
| 249 |
+
"llama3": ChatOpenAI(model = "llama3", api_key=api_key, base_url = "https://llm.nrp-nautilus.io/", temperature=0),
|
| 250 |
+
# "deepseek-r1": BaseChatOpenAI(model = "deepseek-r1", api_key=api_key, base_url = "https://llm.nrp-nautilus.io/", temperature=0),
|
| 251 |
+
"qwen3": ChatOpenAI(model = "qwen3", api_key=api_key, base_url = "https://llm.nrp-nautilus.io/", temperature=0),
|
| 252 |
+
"gemma3": ChatOpenAI(model = "gemma3", api_key=api_key, base_url = "https://llm.nrp-nautilus.io/", temperature=0),
|
| 253 |
|
| 254 |
+
}
|
requirements.txt
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
git+https://github.com/boettiger-lab/cng-python
|
| 2 |
-
leafmap
|
| 3 |
ibis-framework[duckdb]==10.3.1
|
| 4 |
lonboard==0.10.4
|
| 5 |
altair==5.3.0
|
|
@@ -14,5 +14,5 @@ langchain==0.2.17
|
|
| 14 |
langchain-community==0.2.19
|
| 15 |
langchain-core==0.2.43
|
| 16 |
langchain-openai==0.1.25
|
| 17 |
-
streamlit==1.
|
| 18 |
minio==7.2.15
|
|
|
|
| 1 |
git+https://github.com/boettiger-lab/cng-python
|
| 2 |
+
leafmap==0.53.3
|
| 3 |
ibis-framework[duckdb]==10.3.1
|
| 4 |
lonboard==0.10.4
|
| 5 |
altair==5.3.0
|
|
|
|
| 14 |
langchain-community==0.2.19
|
| 15 |
langchain-core==0.2.43
|
| 16 |
langchain-openai==0.1.25
|
| 17 |
+
streamlit==1.50.0
|
| 18 |
minio==7.2.15
|