cassiebuhler commited on
Commit
2d92cf6
Β·
1 Parent(s): 02885ef

reviving app

Browse files
Files changed (4) hide show
  1. app/app.py +116 -43
  2. app/utils.py +111 -9
  3. app/variables.py +108 -5
  4. 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,paint):
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
- st.success(explanation)
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
- elif ("fid" and "geom" not in result.columns):
130
- st.write(result) # if we aren't mapping, just print out the data
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
- out = run_sql(prompt,paint)
152
- if ("fid" in out.columns) and (not out.empty):
153
- ids = out['fid'].unique().tolist()
154
- cols = out.columns.tolist()
155
- bounds = out.total_bounds.tolist()
156
- style = tpl_style(ids, paint, pmtiles)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
  else:
158
- ids = []
 
 
159
  except Exception as e:
160
- error_message = f"ERROR: An unexpected error has occured with the following query:\n\n*{prompt}*\n\n which raised the following error:\n\n{type(e)}: {e}\n"
161
- st.warning("Please try again with a different query", icon="⚠️")
162
- st.write(error_message)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 = style, tooltip = True, zoom_to_layer= False)
 
 
 
 
 
 
 
 
 
 
 
 
182
  m.zoom_to_bounds(bounds)
 
183
 
184
  m.to_streamlit()
185
-
186
  with st.expander("πŸ” View/download data"): # adding data table
187
- if 'out' not in locals():
188
- st.dataframe(gdf.drop('geom').head(100).execute(), use_container_width = True)
 
 
 
 
 
 
 
 
 
 
189
  else:
190
- st.dataframe(out.drop('geom',axis = 1), use_container_width = True)
 
 
 
 
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 get_legend(paint):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  """
125
  Generates a legend dictionary with color mapping and formatting adjustments.
126
  """
127
- legend = {cat: color for cat, color in paint['stops']}
128
- position, fontsize, bg_color = 'bottom-left', 15, 'white'
129
- bg_color = 'rgba(255, 255, 255, 0.6)'
130
- fontsize = 12
131
- return legend, position, bg_color, fontsize
 
 
 
 
 
 
 
 
 
 
 
 
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
- "olmo": ChatOpenAI(model = "olmo", api_key=api_key, base_url = "http://ellm.nrp-nautilus.io/", temperature=0),
147
- "llama3": ChatOpenAI(model = "llama3", api_key=api_key, base_url = "http://ellm.nrp-nautilus.io/", temperature=0),
148
- "qwen3": ChatOpenAI(model = "qwen3", api_key=api_key, base_url = "http://ellm.nrp-nautilus.io/", temperature=0),
149
- "gemma3": ChatOpenAI(model = "gemma3", api_key=api_key, base_url = "http://ellm.nrp-nautilus.io/", temperature=0),
 
 
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[maplibre]==0.38.12
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.45.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