ahuang11 commited on
Commit
1f2655e
1 Parent(s): b73468c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +112 -65
app.py CHANGED
@@ -5,19 +5,25 @@ import holoviews as hv
5
  import pandas as pd
6
  import panel as pn
7
  from bokeh.models import HoverTool
 
 
8
  from langchain.callbacks.base import BaseCallbackHandler
9
  from langchain.chat_models import ChatOpenAI
 
 
 
 
 
 
10
 
11
  pn.extension(sizing_mode="stretch_width", notifications=True)
12
  hv.extension("bokeh")
13
 
14
  INSTRUCTIONS = """
15
  #### Name Chronicles lets you explore the history of names in the United States.
16
- - Enter a name to add to plot.
17
- - See stats by hovering a line.
18
- - Click on a line to see the gender distribution.
19
- - Get a random name based on selected criteria.
20
- - Ask AI for some background info on a name.
21
  - Have ideas? [Open an issue](https://github.com/ahuang11/name-chronicles/issues).
22
  """
23
 
@@ -69,6 +75,11 @@ DATA_QUERY = """
69
  ORDER BY name, year
70
  """
71
 
 
 
 
 
 
72
 
73
  class StreamHandler(BaseCallbackHandler):
74
  def __init__(self, container, initial_text="", target_attr="value"):
@@ -84,6 +95,7 @@ class StreamHandler(BaseCallbackHandler):
84
  class NameChronicles:
85
  def __init__(self):
86
  super().__init__()
 
87
  self.db_path = Path("data/names.db")
88
 
89
  # Main
@@ -149,34 +161,27 @@ class NameChronicles:
149
  )
150
 
151
  # AI Widgets
152
- self.ai_key = pn.widgets.PasswordInput(
153
- name="OpenAI Key",
154
- placeholder="",
 
 
 
155
  )
156
- self.ai_prompt = pn.widgets.TextInput(
157
- name="AI Prompt",
158
- value="Share a little history about the name:",
 
 
 
 
159
  )
160
- ai_button = pn.widgets.Button(
161
- name="Get Response",
162
  button_style="outline",
163
  button_type="primary",
 
164
  )
165
- ai_button.on_click(self._prompt_ai)
166
- self.ai_response = pn.widgets.TextAreaInput(
167
- placeholder="",
168
- disabled=True,
169
- height=350,
170
- )
171
- self.ai_pane = pn.Card(
172
- self.ai_key,
173
- self.ai_prompt,
174
- ai_button,
175
- self.ai_response,
176
- collapsed=True,
177
- title="Ask AI",
178
- )
179
-
180
  pn.state.onload(self._initialize_database)
181
 
182
  # Database Methods
@@ -213,6 +218,29 @@ class NameChronicles:
213
  self.names_choice.param.trigger("value")
214
  self.main.objects = [self.holoviews_pane]
215
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
  def _query_names(self, names):
217
  """
218
  Query the database for the given name.
@@ -276,6 +304,21 @@ class NameChronicles:
276
  else:
277
  pn.state.notifications.info("No names found matching the criteria!")
278
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
279
  def _add_name(self, event):
280
  name = event.new.strip().title()
281
  self.names_input.value = ""
@@ -289,45 +332,48 @@ class NameChronicles:
289
  "Maximum of 10 names allowed; please remove some first!"
290
  )
291
  return
292
- value = self.names_choice.value.copy()
293
- options = self.names_choice.options.copy()
294
- if name not in options:
295
- options.append(name)
296
- if name not in value:
297
- value.append(name)
298
- self.names_choice.param.update(
299
- options=options,
300
- value=value,
301
- )
302
 
303
- def _prompt_ai(self, event):
304
- if not self.ai_key.value:
305
- pn.state.notifications.info("Please enter an API key!")
 
 
 
 
 
 
 
 
 
 
 
 
306
  return
307
 
308
- if not self.ai_prompt.value:
309
- pn.state.notifications.info("Please enter a prompt!")
310
  return
311
 
312
- stream_handler = StreamHandler(self.ai_response)
313
- chat = ChatOpenAI(
314
- max_tokens=500,
315
- openai_api_key=self.ai_key.value,
316
- streaming=True,
317
- callbacks=[stream_handler],
318
- )
319
- self.ai_response.loading = True
320
  try:
321
- if self.selection.index:
322
- names = [self._name_indices[self.selection.index[0]]]
323
- else:
324
- names = self.names_choice.value[:3]
325
- chat.predict(f"{self.ai_prompt.value} {names}")
 
 
326
  finally:
327
- self.ai_response.loading = False
328
 
329
  # Plot Methods
330
-
331
  def _click_plot(self, index):
332
  gender_nd_overlay = hv.NdOverlay(kdims=["Gender"])
333
  if not index:
@@ -358,10 +404,6 @@ class NameChronicles:
358
  kdims=["Gender"],
359
  ).opts(legend_position="top_left")
360
 
361
- @staticmethod
362
- def _format_y(value):
363
- return f"{value / 1000}k"
364
-
365
  def _update_plot(self, event):
366
  names = event.new
367
  print(names)
@@ -374,7 +416,7 @@ class NameChronicles:
374
  fontscale=1.28,
375
  xlabel="Year",
376
  ylabel="Count",
377
- yformatter=self._format_y,
378
  legend_limit=0,
379
  padding=(0.2, 0.05),
380
  title="Name Chronicles",
@@ -460,14 +502,19 @@ class NameChronicles:
460
  self.names_choice,
461
  reset_row,
462
  pn.layout.Divider(),
 
 
463
  self.randomize_pane,
464
- self.ai_pane,
465
  data_url,
466
  )
467
  self.main = pn.Column(
468
- pn.widgets.StaticText(value="Loading, this may take a few seconds...", sizing_mode="stretch_both"),
 
 
 
469
  )
470
  template = pn.template.FastListTemplate(
 
471
  sidebar=[sidebar],
472
  main=[self.main],
473
  title="Name Chronicles",
 
5
  import pandas as pd
6
  import panel as pn
7
  from bokeh.models import HoverTool
8
+ from bokeh.models import NumeralTickFormatter
9
+ from pydantic import BaseModel, Field
10
  from langchain.callbacks.base import BaseCallbackHandler
11
  from langchain.chat_models import ChatOpenAI
12
+ from langchain.llms.openai import OpenAI
13
+ from langchain.output_parsers import PydanticOutputParser
14
+ from langchain.pydantic_v1 import BaseModel, Field, validator
15
+ from langchain.memory import ConversationBufferMemory
16
+ from langchain.chains import ConversationChain
17
+ from langchain.prompts import PromptTemplate
18
 
19
  pn.extension(sizing_mode="stretch_width", notifications=True)
20
  hv.extension("bokeh")
21
 
22
  INSTRUCTIONS = """
23
  #### Name Chronicles lets you explore the history of names in the United States.
24
+ - Enter a name to add to plot!
25
+ - Hover over a line for stats or click for the gender distribution.
26
+ - Chat with AI for inspiration or get a random name based on input criteria.
 
 
27
  - Have ideas? [Open an issue](https://github.com/ahuang11/name-chronicles/issues).
28
  """
29
 
 
75
  ORDER BY name, year
76
  """
77
 
78
+ MAX_LLM_COUNT = 2000
79
+
80
+ class FirstNames(BaseModel):
81
+ names: list[str] = Field(description="List of first names")
82
+
83
 
84
  class StreamHandler(BaseCallbackHandler):
85
  def __init__(self, container, initial_text="", target_attr="value"):
 
95
  class NameChronicles:
96
  def __init__(self):
97
  super().__init__()
98
+ self.llm_use_counter = 0
99
  self.db_path = Path("data/names.db")
100
 
101
  # Main
 
161
  )
162
 
163
  # AI Widgets
164
+ self.chat_interface = pn.chat.ChatInterface(
165
+ show_button_name=False,
166
+ callback=self._prompt_ai,
167
+ height=500,
168
+ styles={"background": "white"},
169
+ disabled=True,
170
  )
171
+ self.chat_interface.send(
172
+ value=(
173
+ "Ask me about name suggestions or their history! "
174
+ "To add suggested names, click the button below!"
175
+ ),
176
+ user="System",
177
+ respond=False,
178
  )
179
+ self.parse_ai_button = pn.widgets.Button(
180
+ name="Parse and Add Names",
181
  button_style="outline",
182
  button_type="primary",
183
+ disabled=False,
184
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
  pn.state.onload(self._initialize_database)
186
 
187
  # Database Methods
 
218
  self.names_choice.param.trigger("value")
219
  self.main.objects = [self.holoviews_pane]
220
 
221
+ # Start AI
222
+ self.callback_handler = pn.chat.langchain.PanelCallbackHandler(
223
+ self.chat_interface
224
+ )
225
+ self.chat_openai = ChatOpenAI(
226
+ max_tokens=75,
227
+ streaming=True,
228
+ callbacks=[self.callback_handler],
229
+ )
230
+ self.openai = OpenAI(max_tokens=75)
231
+ memory = ConversationBufferMemory()
232
+ self.conversation_chain = ConversationChain(
233
+ llm=self.chat_openai, memory=memory, callbacks=[self.callback_handler]
234
+ )
235
+ self.chat_interface.disabled = False
236
+ self.parse_ai_button.on_click(self._parse_ai_output)
237
+ self.pydantic_parser = PydanticOutputParser(pydantic_object=FirstNames)
238
+ self.prompt_template = PromptTemplate(
239
+ template="{format_instructions}\n{input}\n",
240
+ input_variables=["input"],
241
+ partial_variables={"format_instructions": self.pydantic_parser.get_format_instructions()},
242
+ )
243
+
244
  def _query_names(self, names):
245
  """
246
  Query the database for the given name.
 
304
  else:
305
  pn.state.notifications.info("No names found matching the criteria!")
306
 
307
+ def _add_only_unique_names(self, names):
308
+ value = self.names_choice.value.copy()
309
+ options = self.names_choice.options.copy()
310
+ for name in names:
311
+ if " " in name:
312
+ name = name.split(" ", 1)[0]
313
+ if name not in options:
314
+ options.append(name)
315
+ if name not in value:
316
+ value.append(name)
317
+ self.names_choice.param.update(
318
+ options=options,
319
+ value=value,
320
+ )
321
+
322
  def _add_name(self, event):
323
  name = event.new.strip().title()
324
  self.names_input.value = ""
 
332
  "Maximum of 10 names allowed; please remove some first!"
333
  )
334
  return
335
+ self._add_only_unique_names([name])
336
+
337
+ async def _prompt_ai(self, contents, user, instance):
338
+ if self.llm_use_counter >= MAX_LLM_COUNT:
339
+ pn.state.notifications.info(
340
+ "Sorry, all the available AI credits have been used!"
341
+ )
342
+ return
 
 
343
 
344
+ prompt = (
345
+ f"One sentence reply to {contents!r} or concisely suggest other relevant names; "
346
+ f"if no name is provided use {self.names_choice.value[-1]!r}."
347
+ )
348
+ self.last_ai_output = await self.conversation_chain.apredict(
349
+ input=prompt,
350
+ callbacks=[self.callback_handler],
351
+ )
352
+ self.llm_use_counter += 1
353
+
354
+ async def _parse_ai_output(self, _):
355
+ if self.llm_use_counter >= MAX_LLM_COUNT:
356
+ pn.state.notifications.info(
357
+ "Sorry, all the available AI credits have been used!"
358
+ )
359
  return
360
 
361
+ if self.last_ai_output is None:
362
+ pn.state.notifications.info("No available AI output to parse!")
363
  return
364
 
 
 
 
 
 
 
 
 
365
  try:
366
+ names_prompt = self.prompt_template.format_prompt(input=self.last_ai_output).to_string()
367
+ names_text = await self.openai.apredict(names_prompt)
368
+ new_names = (await self.pydantic_parser.aparse(names_text)).names
369
+ print(new_names)
370
+ self._add_only_unique_names(new_names)
371
+ except Exception:
372
+ pn.state.notifications.error("Failed to parse AI output.")
373
  finally:
374
+ self.last_ai_output = None
375
 
376
  # Plot Methods
 
377
  def _click_plot(self, index):
378
  gender_nd_overlay = hv.NdOverlay(kdims=["Gender"])
379
  if not index:
 
404
  kdims=["Gender"],
405
  ).opts(legend_position="top_left")
406
 
 
 
 
 
407
  def _update_plot(self, event):
408
  names = event.new
409
  print(names)
 
416
  fontscale=1.28,
417
  xlabel="Year",
418
  ylabel="Count",
419
+ yformatter=NumeralTickFormatter(format="0.0a"),
420
  legend_limit=0,
421
  padding=(0.2, 0.05),
422
  title="Name Chronicles",
 
502
  self.names_choice,
503
  reset_row,
504
  pn.layout.Divider(),
505
+ self.chat_interface,
506
+ self.parse_ai_button,
507
  self.randomize_pane,
 
508
  data_url,
509
  )
510
  self.main = pn.Column(
511
+ pn.widgets.StaticText(
512
+ value="Loading, this may take a few seconds...",
513
+ sizing_mode="stretch_both",
514
+ ),
515
  )
516
  template = pn.template.FastListTemplate(
517
+ sidebar_width=500,
518
  sidebar=[sidebar],
519
  main=[self.main],
520
  title="Name Chronicles",