kiyer commited on
Commit
58d5580
β€’
1 Parent(s): ca30c13

refactored code for speed

Browse files
Files changed (1) hide show
  1. app.py +285 -639
app.py CHANGED
@@ -1,6 +1,9 @@
1
  import streamlit as st
2
  st.set_page_config(layout="wide")
3
 
 
 
 
4
  import numpy as np
5
  from abc import ABC, abstractmethod
6
  from typing import List, Dict, Any, Tuple
@@ -28,6 +31,7 @@ from langchain.text_splitter import RecursiveCharacterTextSplitter
28
  from langchain_core.output_parsers import StrOutputParser
29
  from langchain.callbacks import FileCallbackHandler
30
  from langchain.callbacks.manager import CallbackManager
 
31
 
32
  import instructor
33
  from pydantic import BaseModel, Field
@@ -43,121 +47,51 @@ import faiss
43
  import spacy
44
  from string import punctuation
45
  import pytextrank
46
-
47
- from bokeh.plotting import figure
48
- from bokeh.models import ColumnDataSource
49
- from bokeh.io import output_notebook
50
- from bokeh.palettes import Spectral5
51
- from bokeh.transform import linear_cmap
52
 
53
  ts = time.time()
54
- st.session_state.ts = ts
55
-
56
- openai_key = st.secrets["openai_key"]
57
- cohere_key = st.secrets['cohere_key']
58
 
59
- if 'nlp' not in st.session_state:
 
60
  nlp = spacy.load("en_core_web_sm")
61
  nlp.add_pipe("textrank")
62
- st.session_state.nlp = nlp
63
-
64
- try:
65
- stopwords.words('english')
66
- except:
67
- nltk.download('stopwords')
68
- stopwords.words('english')
 
 
 
 
 
 
 
69
 
70
  st.session_state.gen_llm = openai_llm(temperature=0,
71
- model_name='gpt-4o-mini',
72
  openai_api_key = openai_key)
73
  st.session_state.consensus_client = instructor.patch(OpenAI(api_key=openai_key))
74
  st.session_state.embed_client = OpenAI(api_key = openai_key)
75
  embed_model = "text-embedding-3-small"
76
  st.session_state.embeddings = OpenAIEmbeddings(model = embed_model, api_key = openai_key)
77
 
78
- st.image('local_files/pathfinder_logo.png')
79
-
80
- st.expander("What is Pathfinder / How do I use it?", expanded=False).write(
81
- """
82
- Pathfinder v2.0 is a framework for searching and visualizing astronomy papers on the [arXiv](https://arxiv.org/) and [ADS](https://ui.adsabs.harvard.edu/) using the context
83
- sensitivity from modern large language models (LLMs) to better parse patterns in paper contexts.
84
-
85
- This tool was built during the [JSALT workshop](https://www.clsp.jhu.edu/2024-jelinek-summer-workshop-on-speech-and-language-technology/) to do awesome things.
86
-
87
- **πŸ‘ˆ Use the sidebar to tweak the search parameters to get better results**.
88
-
89
- ### Tool summary:
90
- - Please wait while the initial data loads and compiles, this takes about a minute initially.
91
-
92
- This is not meant to be a replacement to existing tools like the
93
- [ADS](https://ui.adsabs.harvard.edu/),
94
- [arxivsorter](https://www.arxivsorter.org/), semantic search or google scholar, but rather a supplement to find papers
95
- that otherwise might be missed during a literature survey.
96
- It is trained on astro-ph (astrophysics of galaxies) papers up to last-year-ish mined from arxiv and supplemented with ADS metadata,
97
- if you are interested in extending it please reach out!
98
-
99
- Also add: feedback form, socials, literature, contact us, copyright, collaboration, etc.
100
-
101
- The image below shows a representation of all the astro-ph.GA papers that can be explored in more detail
102
- using the `Arxiv embedding` page. The papers tend to cluster together by similarity, and result in an
103
- atlas that shows well studied (forests) and currently uncharted areas (water).
104
- """
105
- )
106
-
107
-
108
- st.sidebar.header("Fine-tune the search")
109
- top_k = st.sidebar.slider("Number of papers to retrieve:", 3, 30, 10)
110
- extra_keywords = st.sidebar.text_input("Enter extra keywords (comma-separated):")
111
-
112
- st.sidebar.subheader("Toggles")
113
- toggle_a = st.sidebar.toggle("Weight by keywords", value = False)
114
- toggle_b = st.sidebar.toggle("Weight by date", value = False)
115
- toggle_c = st.sidebar.toggle("Weight by citations", value = False)
116
-
117
- method = st.sidebar.radio("Retrieval method:", ["Semantic search", "Semantic search + HyDE", "Semantic search + HyDE + CoHERE"], index=2)
118
-
119
- method2 = st.sidebar.radio("Generation complexity:", ["Basic RAG","ReAct Agent"])
120
-
121
- question_type = st.sidebar.selectbox("Select question type:", ["Multi-paper (Default)", "Single-paper", "Bibliometric", "Broad but nuanced"])
122
- st.session_state.question_type = question_type
123
- # store_output = st.sidebar.button("Save output")
124
-
125
- query = st.text_input("Ask me anything:")
126
- submit_button = st.button("Run pathfinder!")
127
-
128
- search_text_list = ['rooting around in the paper pile...','looking for clarity...','scanning the event horizon...','peering into the abyss...','potatoes power this ongoing search...']
129
 
130
- if 'arxiv_corpus' not in st.session_state:
131
- with st.spinner('loading data (please wait for this to finish before querying)...'):
132
- # try:
133
  arxiv_corpus = load_from_disk('data/')
134
- # except:
135
- # st.write('downloading data')
136
- # arxiv_corpus = load_dataset('kiyer/pathfinder_arxiv_data',split='train')
137
- # # arxiv_corpus = load_dataset('kiyer/pathfinder_arxiv_data_galaxy',split='train')
138
- # arxiv_corpus.save_to_disk('data/')
139
- arxiv_corpus.add_faiss_index('embed')
140
- st.session_state.arxiv_corpus = arxiv_corpus
141
- st.toast('loaded arxiv corpus')
142
-
143
- if 'ids' not in st.session_state:
144
- with st.spinner('making the LLM talk to the astro papers...'):
145
- st.session_state.ids = st.session_state.arxiv_corpus['ads_id']
146
- st.session_state.titles = st.session_state.arxiv_corpus['title']
147
- st.session_state.abstracts = st.session_state.arxiv_corpus['abstract']
148
- st.session_state.authors = st.session_state.arxiv_corpus['authors']
149
- st.session_state.cites = st.session_state.arxiv_corpus['cites']
150
- st.session_state.years = st.session_state.arxiv_corpus['date']
151
- st.session_state.kws = st.session_state.arxiv_corpus['keywords']
152
- st.session_state.ads_kws = st.session_state.arxiv_corpus['ads_keywords']
153
- st.session_state.bibcode = st.session_state.arxiv_corpus['bibcode']
154
- st.session_state.umap_x = st.session_state.arxiv_corpus['umap_x']
155
- st.session_state.umap_y = st.session_state.arxiv_corpus['umap_y']
156
- st.toast('done caching. time taken: %.2f sec' %(time.time()-ts))
157
 
158
  def get_keywords(text):
159
  result = []
160
  pos_tag = ['PROPN', 'ADJ', 'NOUN']
 
 
161
  doc = st.session_state.nlp(text.lower())
162
  for token in doc:
163
  if(token.text in st.session_state.nlp.Defaults.stop_words or token.text in punctuation):
@@ -166,43 +100,19 @@ def get_keywords(text):
166
  result.append(token.text)
167
  return result
168
 
169
- def parse_doc(text, nret = 10):
170
- local_kws = []
171
- doc = st.session_state.nlp(text)
172
- # examine the top-ranked phrases in the document
173
- for phrase in doc._.phrases[:nret]:
174
- # print(phrase.text)
175
- local_kws.append(phrase.text)
176
- return local_kws
177
 
178
- class EmbeddingRetrievalSystem():
179
 
180
- def __init__(self, weight_citation = False, weight_date = False, weight_keywords = False):
181
 
182
- self.ids = st.session_state.ids
183
- self.years = st.session_state.years
184
- self.abstract = st.session_state.abstracts
 
 
185
  self.client = OpenAI(api_key = openai_key)
186
  self.embed_model = "text-embedding-3-small"
187
- self.dataset = st.session_state.arxiv_corpus
188
- self.kws = st.session_state.kws
189
- self.cites = st.session_state.cites
190
-
191
- self.weight_citation = weight_citation
192
- self.weight_date = weight_date
193
- self.weight_keywords = weight_keywords
194
- self.id_to_index = {self.ids[i]: i for i in range(len(self.ids))}
195
-
196
  self.generation_client = openai_llm(temperature=0,model_name='gpt-4o-mini', openai_api_key = openai_key)
197
-
198
- # self.citation_filter = CitationFilter(self.dataset)
199
- # self.date_filter = DateFilter(self.dataset['date'])
200
- # self.keyword_filter = KeywordFilter(corpus=self.dataset, remove_capitals=True)
201
-
202
- def parse_date(self, id):
203
- # indexval = np.where(self.ids == id)[0][0]
204
- indexval = id
205
- return self.years[indexval]
206
 
207
  def make_embedding(self, text):
208
  str_embed = self.client.embeddings.create(input = [text], model = self.embed_model).data[0].embedding
@@ -215,39 +125,29 @@ class EmbeddingRetrievalSystem():
215
  def get_query_embedding(self, query):
216
  return self.make_embedding(query)
217
 
218
- def analyze_temporal_query(self, query):
219
- return
220
-
221
  def calc_faiss(self, query_embedding, top_k = 100):
222
  # xq = query_embedding.reshape(-1,1).T.astype('float32')
223
  # D, I = self.index.search(xq, top_k)
224
  # return I[0], D[0]
225
  tmp = self.dataset.search('embed', query_embedding, k=top_k)
226
- return [tmp.indices, tmp.scores]
227
-
228
- def rank_and_filter(self, query, query_embedding, query_date, top_k = 10, return_scores=False, time_result=None):
229
-
230
- # st.write('status')
231
 
232
- # st.write('toggles', self.toggles)
233
- # st.write('question_type', self.question_type)
234
- # st.write('rag method', self.rag_method)
235
- # st.write('gen method', self.gen_method)
236
 
237
  self.weight_keywords = self.toggles["Keyword weighting"]
238
  self.weight_date = self.toggles["Time weighting"]
239
  self.weight_citation = self.toggles["Citation weighting"]
240
 
241
- topk_indices, similarities = self.calc_faiss(np.array(query_embedding), top_k = 1000)
242
  similarities = 1/similarities # converting from a distance (less is better) to a similarity (more is better)
243
 
244
- query_kws = get_keywords(query)
245
- input_kws = self.query_input_keywords
246
- query_kws = query_kws + input_kws
247
- self.query_kws = query_kws
248
-
249
  if self.weight_keywords == True:
250
- sub_kws = [self.kws[i] for i in topk_indices]
 
 
 
 
 
251
  kw_weight = np.zeros((len(topk_indices),)) + 0.1
252
 
253
  for k in query_kws:
@@ -263,7 +163,7 @@ class EmbeddingRetrievalSystem():
263
  kw_weight = np.ones((len(topk_indices),))
264
 
265
  if self.weight_date == True:
266
- sub_dates = [self.years[i] for i in topk_indices]
267
  date = datetime.now().date()
268
  date_diff = np.array([((date - i).days / 365.) for i in sub_dates])
269
  # age_weight = (1 + np.exp(date_diff/2.1))**(-1) + 0.5
@@ -274,7 +174,7 @@ class EmbeddingRetrievalSystem():
274
 
275
  if self.weight_citation == True:
276
  # st.write('weighting by citations')
277
- sub_cites = np.array([self.cites[i] for i in topk_indices])
278
  temp = sub_cites.copy()
279
  temp[sub_cites > 300] = 300.
280
  cite_weight = (1 + np.exp((300-temp)/42.0))**(-1.)
@@ -287,102 +187,24 @@ class EmbeddingRetrievalSystem():
287
  filtered_results = [[topk_indices[i], similarities[i]] for i in range(len(similarities))]
288
  top_results = sorted(filtered_results, key=lambda x: x[1], reverse=True)[:top_k]
289
 
 
 
 
 
290
  if return_scores:
291
- return {doc[0]: doc[1] for doc in top_results}
292
 
293
  # Only keep the document IDs
294
  top_results = [doc[0] for doc in top_results]
295
- return top_results
296
-
297
- def retrieve(self, query, top_k, time_result=None, query_date = None, return_scores = False):
298
-
299
- query_embedding = self.get_query_embedding(query)
300
-
301
- # Judge time relevance
302
- if time_result is None:
303
- if self.weight_date:
304
- time_result, time_taken = self.analyze_temporal_query(query, self.anthropic_client)
305
- else:
306
- time_result = {'has_temporal_aspect': False, 'expected_year_filter': None, 'expected_recency_weight': None}
307
-
308
- top_results = self.rank_and_filter(query,
309
- query_embedding,
310
- query_date,
311
- top_k,
312
- return_scores = return_scores,
313
- time_result = time_result)
314
-
315
- return top_results
316
-
317
- class HydeRetrievalSystem(EmbeddingRetrievalSystem):
318
- def __init__(self, generation_model: str = "claude-3-haiku-20240307",
319
- embedding_model: str = "text-embedding-3-small",
320
- temperature: float = 0.5,
321
- max_doclen: int = 500,
322
- generate_n: int = 1,
323
- embed_query = True,
324
- conclusion = False, **kwargs):
325
-
326
- # Handle the kwargs for the superclass init -- filters/citation weighting
327
- super().__init__(**kwargs)
328
-
329
- if max_doclen * generate_n > 8191:
330
- raise ValueError("Too many tokens. Please reduce max_doclen or generate_n.")
331
-
332
- self.embedding_model = embedding_model
333
- self.generation_model = generation_model
334
-
335
- # HYPERPARAMETERS
336
- self.temperature = temperature # generation temperature
337
- self.max_doclen = max_doclen # max tokens for generation
338
- self.generate_n = generate_n # how many documents
339
- self.embed_query = embed_query # embed the query vector?
340
- self.conclusion = conclusion # generate conclusion as well?
341
-
342
- # self.anthropic_key = anthropic_key
343
- # self.generation_client = anthropic.Anthropic(api_key = self.anthropic_key)
344
-
345
-
346
- def retrieve(self, query: str, top_k: int = 10, return_scores = False, time_result = None) -> List[Tuple[str, str, float]]:
347
- if time_result is None:
348
- if self.weight_date: time_result, time_taken = analyze_temporal_query(query, self.anthropic_client)
349
- else: time_result = {'has_temporal_aspect': False, 'expected_year_filter': None, 'expected_recency_weight': None}
350
-
351
- docs = self.generate_docs(query)
352
- st.expander('Abstract generated with hyde', expanded=False).write(docs)
353
-
354
- doc_embeddings = self.embed_docs(docs)
355
-
356
- if self.embed_query:
357
- query_emb = self.embed_docs([query])[0]
358
- doc_embeddings.append(query_emb)
359
-
360
- embedding = np.mean(np.array(doc_embeddings), axis = 0)
361
-
362
- top_results = self.rank_and_filter(query, embedding, query_date=None, top_k = top_k, return_scores = return_scores, time_result = time_result)
363
-
364
- return top_results
365
 
366
  def generate_doc(self, query: str):
367
  prompt = """You are an expert astronomer. Given a scientific query, generate the abstract of an expert-level research paper
368
  that answers the question. Stick to a maximum length of {} tokens and return just the text of the abstract and conclusion.
369
  Do not include labels for any section. Use research-specific jargon.""".format(self.max_doclen)
370
- # st.write('invoking hyde generation')
371
-
372
- # message = self.generation_client.messages.create(
373
- # model = self.generation_model,
374
- # max_tokens = self.max_doclen,
375
- # temperature = self.temperature,
376
- # system = prompt,
377
- # messages=[{ "role": "user",
378
- # "content": [{"type": "text", "text": query,}] }]
379
- # )
380
- # return message.content[0].text
381
 
382
  messages = [("system",prompt,),("human", query),]
383
- return self.generation_client.invoke(messages).content
384
-
385
-
386
 
387
  def generate_docs(self, query: str):
388
  docs = []
@@ -393,120 +215,185 @@ class HydeRetrievalSystem(EmbeddingRetrievalSystem):
393
  def embed_docs(self, docs: List[str]):
394
  return self.embed_batch(docs)
395
 
396
- class HydeCohereRetrievalSystem(HydeRetrievalSystem):
397
- def __init__(self, **kwargs):
398
- super().__init__(**kwargs)
 
399
 
400
- self.cohere_key = cohere_key
401
- self.cohere_client = cohere.Client(self.cohere_key)
402
 
403
- def retrieve(self, query: str,
404
- top_k: int = 10,
405
- rerank_top_k: int = 250,
406
- return_scores = False, time_result = None,
407
- reweight = False) -> List[Tuple[str, str, float]]:
408
 
409
- if time_result is None:
410
- if self.weight_date: time_result, time_taken = analyze_temporal_query(query, self.anthropic_client)
411
- else: time_result = {'has_temporal_aspect': False, 'expected_year_filter': None, 'expected_recency_weight': None}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
412
 
413
- top_results = super().retrieve(query, top_k = rerank_top_k, time_result = time_result)
414
 
415
- # doc_texts = self.get_document_texts(top_results)
416
- # docs_for_rerank = [f"Abstract: {doc['abstract']}\nConclusions: {doc['conclusions']}" for doc in doc_texts]
417
- docs_for_rerank = [self.abstract[i] for i in top_results]
418
 
419
- if len(docs_for_rerank) == 0:
420
- return []
 
 
 
 
 
 
421
 
422
- reranked_results = self.cohere_client.rerank(
423
- query=query,
424
- documents=docs_for_rerank,
425
- model='rerank-english-v3.0',
426
- top_n=top_k
427
- )
 
 
 
428
 
429
- final_results = []
430
- for result in reranked_results.results:
431
- doc_id = top_results[result.index]
432
- doc_text = docs_for_rerank[result.index]
433
- score = float(result.relevance_score)
434
- final_results.append([doc_id, "", score])
435
 
436
- if reweight:
437
- if time_result['has_temporal_aspect']:
438
- final_results = self.date_filter.filter(final_results, time_score = time_result['expected_recency_weight'])
439
 
440
- if self.weight_citation: self.citation_filter.filter(final_results)
441
 
442
- if return_scores:
443
- return {result[0]: result[2] for result in final_results}
444
 
445
- return [doc[0] for doc in final_results]
 
 
 
 
 
446
 
447
- def embed_docs(self, docs: List[str]):
448
- return self.embed_batch(docs)
 
 
 
 
 
 
 
 
 
 
449
 
450
- # --------- other fns ------------------
 
 
 
 
 
 
 
451
 
452
- def get_topk(query, top_k):
453
- print('running retrieval')
454
- rs = st.session_state.ec.retrieve(query, top_k, return_scores=True)
455
- return rs
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
456
 
457
- def Library(query, top_k = 7):
458
- rs = get_topk(query, top_k = top_k)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
459
  op_docs = ''
460
- for paperno, i in enumerate(rs):
461
- op_docs = op_docs + 'Paper %.0f:' %(paperno+1) +' (published in '+st.session_state.bibcode[i][0:4] + ') ' + st.session_state.titles[i] + '\n' + st.session_state.abstracts[i] + '\n\n'
462
 
463
  return op_docs
464
 
465
- def Library2(query, top_k = 7):
466
- rs = get_topk(query, top_k = top_k)
467
- absts, fnames = [], []
468
- for paperno, i in enumerate(rs):
469
- absts.append(st.session_state.abstracts[i])
470
- fnames.append(st.session_state.bibcode[i])
471
- return absts, fnames, rs
472
-
473
- def get_paper_df(ids):
474
-
475
- papers, scores, yrs, links, cites, kws, authors, absts = [], [], [], [], [], [], [], []
476
- for i in ids:
477
- papers.append(st.session_state.titles[i])
478
- scores.append(ids[i])
479
- links.append('https://ui.adsabs.harvard.edu/abs/'+st.session_state.bibcode[i]+'/abstract')
480
- yrs.append(st.session_state.bibcode[i][0:4])
481
- cites.append(st.session_state.cites[i])
482
- authors.append(st.session_state.authors[i][0])
483
- kws.append(st.session_state.ads_kws[i])
484
- absts.append(st.session_state.abstracts[i])
485
-
486
- return pd.DataFrame({
487
- 'Title': papers,
488
- 'Relevance': scores,
489
- 'Lead author': authors,
490
- 'Year': yrs,
491
- 'ADS Link': links,
492
- 'Citations': cites,
493
- 'Keywords': kws,
494
- 'Abstract': absts
495
- })
496
-
497
- def extract_keywords(question, ec):
498
- # Simulated keyword extraction (replace with actual logic)
499
- return ['keyword1', 'keyword2', 'keyword3']
500
-
501
- # Function to estimate consensus (replace with actual implementation)
502
- def estimate_consensus():
503
- # Simulated consensus estimation (replace with actual calculation)
504
- return 0.75
505
-
506
-
507
- def run_agent_qa(query, top_k):
508
-
509
- # define tools
510
  search = DuckDuckGoSearchAPIWrapper()
511
  tools = [
512
  Tool(
@@ -524,42 +411,8 @@ def run_agent_qa(query, top_k):
524
  if 'tools' not in st.session_state:
525
  st.session_state.tools = tools
526
 
527
- # define prompt
528
-
529
- # for another question type:
530
- # First, find the quotes from the document that are most relevant to answering the question, and then print them in numbered order.
531
- # Quotes should be relatively short. If there are no relevant quotes, write β€œNo relevant quotes” instead.
532
-
533
-
534
- template = """You are an expert astronomer and cosmologist.
535
- Answer the following question as best you can using information from the library, but speaking in a concise and factual manner.
536
- If you can not come up with an answer, say you do not know.
537
- Try to break the question down into smaller steps and solve it in a logical manner.
538
-
539
- You have access to the following tools:
540
-
541
- {tools}
542
-
543
- Use the following format:
544
-
545
- Question: the input question you must answer
546
- Thought: you should always think about what to do
547
- Action: the action to take, should be one of [{tool_names}]
548
- Action Input: the input to the action
549
- Observation: the result of the action
550
- ... (this Thought/Action/Action Input/Observation can repeat N times)
551
- Thought: I now know the final answer
552
- Final Answer: the final answer to the original input question. provide information about how you arrived at the answer, and any nuances or uncertainties the reader should be aware of
553
-
554
- Begin! Remember to speak in a pedagogical and factual manner."
555
-
556
- Question: {input}
557
- Thought:{agent_scratchpad}"""
558
-
559
  prompt = hub.pull("hwchase17/react")
560
- prompt.template=template
561
-
562
- # path to write intermediate trace to
563
 
564
  file_path = "agent_trace.txt"
565
  try:
@@ -569,8 +422,6 @@ def run_agent_qa(query, top_k):
569
  file_handler = FileCallbackHandler(file_path)
570
  callback_manager=CallbackManager([file_handler])
571
 
572
- # define and execute agent
573
-
574
  tool_names = [tool.name for tool in st.session_state.tools]
575
  if 'agent' not in st.session_state:
576
  # agent = ZeroShotAgent(llm_chain=llm_chain, allowed_tools=tool_names)
@@ -584,125 +435,27 @@ def run_agent_qa(query, top_k):
584
  answer = st.session_state.agent_executor.invoke({"input": query,})
585
  return answer
586
 
587
- regular_prompt = """You are an expert astronomer and cosmologist.
588
- Answer the following question as best you can using information from the library, but speaking in a concise and factual manner.
589
- If you can not come up with an answer, say you do not know.
590
- Try to break the question down into smaller steps and solve it in a logical manner.
591
 
592
- Provide information about how you arrived at the answer, and any nuances or uncertainties the reader should be aware of.
593
-
594
- Begin! Remember to speak in a pedagogical and factual manner."
595
-
596
- Relevant documents:{context}
597
-
598
- Question: {question}
599
- Answer:"""
600
-
601
- bibliometric_prompt = """You are an AI assistant with expertise in astronomy and astrophysics literature. Your task is to assist with relevant bibliometric information in response to a user question. The user question may consist of identifying key papers, authors, or trends in a specific area of astronomical research.
602
-
603
- Depending on what the user wants, direct them to consult the NASA Astrophysics Data System (ADS) at https://ui.adsabs.harvard.edu/. Provide them with the recommended ADS query depending on their question.
604
-
605
- Here's a more detailed guide on how to use NASA ADS for various types of queries:
606
-
607
- Basic topic search: Enter keywords in the search bar, e.g., "exoplanets". Use quotation marks for exact phrases, e.g., "dark energy”
608
- Author search: Use the syntax author:"Last Name, First Name", e.g., author:"Hawking, S". For papers by multiple authors, use AND, e.g., author:"Hawking, S" AND author:"Ellis, G"
609
- Date range: Use year:YYYY-YYYY, e.g., year:2010-2020. For papers since a certain year, use year:YYYY-, e.g., year:2015-
610
- 4.Combining search terms: Use AND, OR, NOT operators, e.g., "black holes" AND (author:"Hawking, S" OR author:"Penrose, R")
611
- Filtering results: Use the left sidebar to filter by publication year, article type, or astronomy database
612
- Sorting results: Use the "Sort" dropdown menu to order by options like citation count, publication date, or relevance
613
- Advanced searches: Click on the "Search" dropdown menu and select "Classic Form" for field-specific searchesUse bibcode:YYYY for a specific journal/year, e.g., bibcode:2020ApJ to find all Astrophysical Journal papers from 2020
614
- Finding review articles: Wrap the query in the reviews() operator (e.g. reviews(β€œdark energy”))
615
- Excluding preprints: Add NOT doctype:"eprint" to your search
616
- Citation metrics: Click on the citation count of a paper to see its citation history and who has cited it
617
-
618
- Some examples:
619
-
620
- Example 1:
621
- β€œHow many papers published in 2022 used data from MAST missions?”
622
- Your response should be: year:2022 data:"MAST"
623
-
624
- Example 2:
625
- β€œWhat are the most cited papers on spiral galaxy halos measured in X-rays, with publication date from 2010 to 2023?
626
- Your response should be: "spiral galaxy halos" AND "x-ray" year:2010-2024
627
-
628
- Example 3:
629
- β€œCan you list 3 papers published by β€œ< name>” as first author?”
630
- Your response should be: author: β€œ^X”
631
-
632
- Example 4:
633
- β€œBased on papers with β€œ<name>” as an author or co-author, can you suggest the five most recent astro-ph papers that would be relevant?”
634
- Your response should be:
635
-
636
- Remember to advise users that while these examples cover many common scenarios, NASA ADS has many more advanced features that can be explored through its documentation.
637
-
638
- Relevant documents:{context}
639
- Question: {question}
640
-
641
- Response:"""
642
-
643
- single_paper_prompt = """You are an astronomer with access to a vast database of astronomical facts and figures. Your task is to provide a concise, accurate answer to the following specific factual question about astronomy or astrophysics.
644
- Provide the requested information clearly and directly. If relevant, include the source of your information or any recent updates to this fact. If there's any uncertainty or variation in the accepted value, briefly explain why.
645
- If the question can't be answered with a single fact, provide a short, focused explanation. Always prioritize accuracy over speculation.
646
- Relevant documents:{context}
647
- Question: {question}
648
- Response:"""
649
-
650
- deep_knowledge_prompt = """You are an expert astronomer with deep knowledge across various subfields of astronomy and astrophysics. Your task is to provide a comprehensive and nuanced answer to the following question, which involves an unresolved topic or requires broad, common-sense understanding.
651
- Consider multiple perspectives and current debates in the field. Explain any uncertainties or ongoing research. If relevant, mention how this topic connects to other areas of astronomy.
652
- Provide your response in a clear, pedagogical manner, breaking down complex concepts for easier understanding. If appropriate, suggest areas where further research might be needed.
653
- After formulating your initial response, take a moment to reflect on your answer. Consider:
654
- 1. Have you addressed all aspects of the question?
655
- 2. Are there any potential biases or assumptions in your explanation?
656
- 3. Is your explanation clear and accessible to someone with a general science background?
657
- 4. Have you adequately conveyed the uncertainties or debates surrounding this topic?
658
- Based on this reflection, refine your answer as needed.
659
- Remember, while you have extensive knowledge, it's okay to acknowledge the limits of current scientific understanding. If parts of the question cannot be answered definitively, explain why.
660
- Relevant documents:{context}
661
-
662
- Question: {question}
663
-
664
- Initial Response:
665
- [Your initial response here]
666
-
667
- Reflection and Refinement:
668
- [Your reflections and any refinements to your answer here]
669
-
670
- Final Response:
671
- [Your final, refined answer here]"""
672
 
673
- def make_rag_qa_answer(query, top_k = 10):
 
674
 
 
 
 
 
 
 
675
 
676
- # try:
677
- absts, fhdrs, rs = Library2(query, top_k = top_k)
678
-
679
- temp_abst = ''
680
- loaders = []
681
- for i in range(len(absts)):
682
- temp_abst = absts[i]
683
-
684
- try:
685
- text_file = open("absts/"+fhdrs[i]+".txt", "w")
686
- except:
687
- os.mkdir('absts')
688
- text_file = open("absts/"+fhdrs[i]+".txt", "w")
689
- n = text_file.write(temp_abst)
690
- text_file.close()
691
- loader = TextLoader("absts/"+fhdrs[i]+".txt")
692
- loaders.append(loader)
693
-
694
  text_splitter = RecursiveCharacterTextSplitter(chunk_size=150, chunk_overlap=50, add_start_index=True)
695
-
696
- splits = text_splitter.split_documents([loader.load()[0] for loader in loaders])
697
  vectorstore = Chroma.from_documents(documents=splits, embedding=st.session_state.embeddings, collection_name='retdoc4')
698
  # retriever = vectorstore.as_retriever(search_type="similarity", search_kwargs={"k": 6, "fetch_k": len(splits)})
699
  retriever = vectorstore.as_retriever(search_type="similarity", search_kwargs={"k": 6})
700
-
701
- for i in range(len(absts)):
702
- try:
703
- os.remove("absts/"+fhdrs[i]+".txt")
704
- except:
705
- print("absts/"+fhdrs[i]+".txt not found")
706
 
707
  if st.session_state.question_type == 'Bibliometric':
708
  template = bibliometric_prompt
@@ -713,79 +466,33 @@ def make_rag_qa_answer(query, top_k = 10):
713
  else:
714
  template = regular_prompt
715
  prompt = PromptTemplate.from_template(template)
716
-
717
  def format_docs(docs):
718
  return "\n\n".join(doc.page_content for doc in docs)
719
-
720
-
721
  rag_chain_from_docs = (
722
  RunnablePassthrough.assign(context=(lambda x: format_docs(x["context"])))
723
  | prompt
724
  | st.session_state.gen_llm
725
  | StrOutputParser()
726
  )
727
-
728
  rag_chain_with_source = RunnableParallel(
729
  {"context": retriever, "question": RunnablePassthrough()}
730
  ).assign(answer=rag_chain_from_docs)
731
-
732
  rag_answer = rag_chain_with_source.invoke(query, )
733
-
734
  vectorstore.delete_collection()
735
 
736
- # except:
737
- # st.write('heavy load! please wait 10 seconds and try again.')
738
-
739
- return rag_answer, rs
740
 
741
  def guess_question_type(query: str):
742
- categorization_prompt = """You are an expert astrophysicist and computer scientist specializing in linguistics and semantics. Your task is to categorize a given query into one of the following categories:
743
-
744
- 1. Summarization
745
- 2. Single-paper factual
746
- 3. Multi-paper factual
747
- 4. Named entity recognition
748
- 5. Jargon-specific questions / overloaded words
749
- 6. Time-sensitive
750
- 7. Consensus evaluation
751
- 8. What-ifs and counterfactuals
752
- 9. Compositional
753
-
754
- Analyze the query carefully, considering its content, structure, and implications. Then, determine which of the above categories best fits the query.
755
-
756
- In your analysis, consider the following:
757
- - Does the query ask for a well-known datapoint or mechanism?
758
- - Can it be answered by a single paper or does it require multiple sources?
759
- - Does it involve proper nouns or specific scientific terms?
760
- - Is it time-dependent or likely to change in the near future?
761
- - Does it require evaluating consensus across multiple sources?
762
- - Is it a hypothetical or counterfactual question?
763
- - Does it need to be broken down into sub-queries (i.e. compositional)?
764
-
765
- After your analysis, categorize the query into one of the nine categories listed above.
766
-
767
- Provide a brief explanation for your categorization, highlighting the key aspects of the query that led to your decision.
768
-
769
- Present your final answer in the following format:
770
-
771
- <categorization>
772
- Category: [Selected category]
773
- Explanation: [Your explanation for the categorization]
774
- </categorization>"""
775
- # st.write('invoking hyde generation')
776
-
777
- # message = self.generation_client.messages.create(
778
- # model = self.generation_model,
779
- # max_tokens = self.max_doclen,
780
- # temperature = self.temperature,
781
- # system = prompt,
782
- # messages=[{ "role": "user",
783
- # "content": [{"type": "text", "text": query,}] }]
784
- # )
785
- # return message.content[0].text
786
-
787
- messages = [("system",categorization_prompt,),("human", query),]
788
- return st.session_state.ec.generation_client.invoke(messages).content
789
 
790
  class OverallConsensusEvaluation(BaseModel):
791
  consensus: Literal["Strong Agreement", "Moderate Agreement", "Weak Agreement", "No Clear Consensus", "Weak Disagreement", "Moderate Disagreement", "Strong Disagreement"] = Field(
@@ -840,141 +547,83 @@ def evaluate_overall_consensus(query: str, abstracts: List[str]) -> OverallConse
840
 
841
  return response
842
 
843
- def create_embedding_plot(rs):
844
- """
845
- function to create embedding plot
846
- """
847
-
848
- pltsource = ColumnDataSource(data=dict(
849
- x=st.session_state.umap_x,
850
- y=st.session_state.umap_y,
851
- title=st.session_state.titles,
852
- link=st.session_state.bibcode,
853
- ))
854
-
855
- rsflag = np.zeros((len(st.session_state.ids),))
856
- rsflag[np.array([k for k in rs])] = 1
857
-
858
- # outflag = np.zeros((len(st.session_state.ids),))
859
- # outflag[np.array([k for k in find_outliers(rs)])] = 1
860
- pltsource.data['colors'] = rsflag * 0.8 + 0.1
861
- # pltsource.data['colors'][outflag] = 0.5
862
- pltsource.data['sizes'] = (rsflag + 1)**5 / 100
863
-
864
- TOOLTIPS = """
865
- <div style="width:300px;">
866
- ID: $index
867
- ($x, $y)
868
- @title <br>
869
- @link <br> <br>
870
- </div>
871
- """
872
-
873
- mapper = linear_cmap(field_name="colors", palette=Spectral5, low=0., high=1.)
874
-
875
- p = figure(width=700, height=900, tooltips=TOOLTIPS, x_range=(0, 20), y_range=(-4.2,18),
876
- title="UMAP projection of embeddings for the astro-ph corpus")
877
 
878
- p.axis.visible=False
879
- p.grid.visible=False
880
- p.outline_line_alpha = 0.
881
 
882
- p.circle('x', 'y', radius='sizes', source=pltsource, alpha=0.3, fill_color=mapper, fill_alpha='colors', line_color="lightgrey",line_alpha=0.1)
883
 
884
- return p
885
 
886
- if submit_button:
887
-
888
- keywords = [kw.strip() for kw in extra_keywords.split(',')] if extra_keywords else []
889
- toggles = {'Keyword weighting': toggle_a, 'Time weighting': toggle_b, 'Citation weighting': toggle_c}
890
-
891
- if (method == "Semantic search"):
892
- with st.spinner('set retrieval method to'+ method):
893
- st.session_state.ec = EmbeddingRetrievalSystem()
894
- elif (method == "Semantic search + HyDE"):
895
- with st.spinner('set retrieval method to'+ method):
896
- st.session_state.ec = HydeRetrievalSystem()
897
- elif (method == "Semantic search + HyDE + CoHERE"):
898
- with st.spinner('set retrieval method to'+ method):
899
- st.session_state.ec = HydeCohereRetrievalSystem()
900
- st.toast('loaded retrieval system')
901
-
902
  with st.spinner(search_text_list[np.random.choice(len(search_text_list))]):
 
903
 
904
- st.session_state.ec.query_input_keywords = keywords
905
- st.session_state.ec.toggles = toggles
906
- st.session_state.ec.question_type = question_type
907
- st.session_state.ec.rag_method = method
908
- st.session_state.ec.gen_method = method2
909
-
910
- if method2 == "Basic RAG":
911
- st.session_state.gen_method = 'rag'
912
- elif method2 == "ReAct Agent":
913
- st.session_state.gen_method = 'agent'
914
 
915
- if st.session_state.gen_method == 'agent':
916
- answer = run_agent_qa(query, top_k)
917
- rs = get_topk(query, top_k)
918
 
 
 
919
  answer_text = answer['output']
 
920
  st.write(answer_text)
921
-
922
  file_path = "agent_trace.txt"
923
  with open(file_path, 'r') as file:
924
  intermediate_steps = file.read()
925
  st.expander('Intermediate steps', expanded=False).write(intermediate_steps)
926
 
927
  elif st.session_state.gen_method == 'rag':
928
- answer, rs = make_rag_qa_answer(query, top_k)
 
929
  answer_text = answer['answer']
930
  st.write(answer_text)
931
 
932
- triggered_keywords = st.session_state.ec.query_kws
933
-
934
- with st.spinner('compiling top-k papers'+ method):
935
- papers_df = get_paper_df(rs)
936
-
937
- with st.expander("Relevant papers", expanded=True):
938
- # st.dataframe(papers_df, hide_index=True)
939
- st.data_editor(papers_df, column_config = {'ADS Link':st.column_config.LinkColumn(display_text= 'https://ui.adsabs.harvard.edu/abs/(.*?)/abstract')})
940
-
941
  st.write('**Triggered keywords:** `'+ "`, `".join(triggered_keywords)+'`')
942
-
943
  col1, col2 = st.columns(2)
944
 
945
  with col1:
946
- with st.expander("Evaluating question type", expanded=True):
947
- st.subheader("Question type suggestion")
948
- question_type_gen = guess_question_type(query)
949
- if '<categorization>' in question_type_gen:
950
- question_type_gen = question_type_gen.split('<categorization>')[1]
951
- if '</categorization>' in question_type_gen:
952
- question_type_gen = question_type_gen.split('</categorization>')[0]
953
- question_type_gen = question_type_gen.replace('\n',' \n')
954
- st.markdown(question_type_gen)
 
955
 
956
  with col2:
957
- with st.expander("Evaluating abstract consensus", expanded=True):
958
- consensus_answer = evaluate_overall_consensus(query, [st.session_state.abstracts[i] for i in rs])
959
- st.subheader("Consensus: "+consensus_answer.consensus)
960
- st.markdown(consensus_answer.explanation)
961
- st.markdown('Relevance of retrieved papers to answer: %.1f' %consensus_answer.relevance_score)
962
-
 
963
  session_vars = {
964
  "runtime": "pathfinder_v1_online",
965
  "query": query,
966
  "question_type": question_type,
967
- 'Keyword weighting': toggle_a,
968
- 'Time weighting': toggle_b,
969
  'Citation weighting': toggle_c,
970
  "rag_method" : method,
971
  "gen_method" : method2,
972
  "answer" : answer_text,
973
- "topk" : ['%.0f' %i for i in rs],
974
- "topk_scores" : ['%.6f' %rs[i] for i in rs],
 
 
975
  "topk_papers": list(papers_df['ADS Link']),
976
  }
977
-
978
  @st.fragment()
979
  def download_op(data):
980
  json_string = json.dumps(data)
@@ -983,12 +632,9 @@ if submit_button:
983
  file_name="pathfinder_data.json",
984
  mime="application/json",
985
  data=json_string,)
986
-
987
- with st.sidebar:
988
- download_op(session_vars)
989
-
990
- # embedding_plot = create_embedding_plot(rs)
991
- # st.bokeh_chart(embedding_plot)
992
-
993
  else:
994
  st.info("Use the sidebar to tweak the search parameters to get better results.")
 
1
  import streamlit as st
2
  st.set_page_config(layout="wide")
3
 
4
+ openai_key = st.secrets["openai_key"]
5
+ cohere_key = st.secrets['cohere_key']
6
+
7
  import numpy as np
8
  from abc import ABC, abstractmethod
9
  from typing import List, Dict, Any, Tuple
 
31
  from langchain_core.output_parsers import StrOutputParser
32
  from langchain.callbacks import FileCallbackHandler
33
  from langchain.callbacks.manager import CallbackManager
34
+ from langchain.schema import Document
35
 
36
  import instructor
37
  from pydantic import BaseModel, Field
 
47
  import spacy
48
  from string import punctuation
49
  import pytextrank
50
+ from prompts import *
 
 
 
 
 
51
 
52
  ts = time.time()
 
 
 
 
53
 
54
+ @st.cache_resource
55
+ def load_nlp():
56
  nlp = spacy.load("en_core_web_sm")
57
  nlp.add_pipe("textrank")
58
+ try:
59
+ stopwords.words('english')
60
+ except:
61
+ nltk.download('stopwords')
62
+ stopwords.words('english')
63
+ return nlp
64
+
65
+ # @st.cache_resource
66
+ # def load_embeddings():
67
+ # return OpenAIEmbeddings(model="text-embedding-3-small", api_key=st.secrets["openai_key"])
68
+ #
69
+ # @st.cache_resource
70
+ # def load_llm():
71
+ # return ChatOpenAI(temperature=0, model_name='gpt-4o-mini', openai_api_key=st.secrets["openai_key"])
72
 
73
  st.session_state.gen_llm = openai_llm(temperature=0,
74
+ model_name='gpt-4o-mini',
75
  openai_api_key = openai_key)
76
  st.session_state.consensus_client = instructor.patch(OpenAI(api_key=openai_key))
77
  st.session_state.embed_client = OpenAI(api_key = openai_key)
78
  embed_model = "text-embedding-3-small"
79
  st.session_state.embeddings = OpenAIEmbeddings(model = embed_model, api_key = openai_key)
80
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
 
82
+ # @st.cache_data
83
+ def load_arxiv_corpus():
84
+ with st.spinner('loading astro-ph corpus'):
85
  arxiv_corpus = load_from_disk('data/')
86
+ arxiv_corpus.load_faiss_index('embed', 'data/astrophindex.faiss')
87
+ st.toast('loaded data. time taken: %.2f sec' %(time.time()-ts))
88
+ return arxiv_corpus
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
 
90
  def get_keywords(text):
91
  result = []
92
  pos_tag = ['PROPN', 'ADJ', 'NOUN']
93
+ if 'nlp' not in st.session_state:
94
+ st.session_state.nlp = load_nlp()
95
  doc = st.session_state.nlp(text.lower())
96
  for token in doc:
97
  if(token.text in st.session_state.nlp.Defaults.stop_words or token.text in punctuation):
 
100
  result.append(token.text)
101
  return result
102
 
 
 
 
 
 
 
 
 
103
 
 
104
 
 
105
 
106
+ class RetrievalSystem():
107
+
108
+ def __init__(self):
109
+
110
+ self.dataset = st.session_state.arxiv_corpus
111
  self.client = OpenAI(api_key = openai_key)
112
  self.embed_model = "text-embedding-3-small"
 
 
 
 
 
 
 
 
 
113
  self.generation_client = openai_llm(temperature=0,model_name='gpt-4o-mini', openai_api_key = openai_key)
114
+ self.hyde_client = openai_llm(temperature=0.5,model_name='gpt-4o-mini', openai_api_key = openai_key)
115
+ self.cohere_client = cohere.Client(cohere_key)
 
 
 
 
 
 
 
116
 
117
  def make_embedding(self, text):
118
  str_embed = self.client.embeddings.create(input = [text], model = self.embed_model).data[0].embedding
 
125
  def get_query_embedding(self, query):
126
  return self.make_embedding(query)
127
 
 
 
 
128
  def calc_faiss(self, query_embedding, top_k = 100):
129
  # xq = query_embedding.reshape(-1,1).T.astype('float32')
130
  # D, I = self.index.search(xq, top_k)
131
  # return I[0], D[0]
132
  tmp = self.dataset.search('embed', query_embedding, k=top_k)
133
+ return [tmp.indices, tmp.scores, self.dataset[tmp.indices]]
 
 
 
 
134
 
135
+ def rank_and_filter(self, query, query_embedding, top_k = 10, top_k_internal = 1000, return_scores=False):
 
 
 
136
 
137
  self.weight_keywords = self.toggles["Keyword weighting"]
138
  self.weight_date = self.toggles["Time weighting"]
139
  self.weight_citation = self.toggles["Citation weighting"]
140
 
141
+ topk_indices, similarities, small_corpus = self.calc_faiss(np.array(query_embedding), top_k = top_k_internal)
142
  similarities = 1/similarities # converting from a distance (less is better) to a similarity (more is better)
143
 
 
 
 
 
 
144
  if self.weight_keywords == True:
145
+
146
+ query_kws = get_keywords(query)
147
+ input_kws = self.query_input_keywords
148
+ query_kws = query_kws + input_kws
149
+ self.query_kws = query_kws
150
+ sub_kws = [small_corpus['keywords'][i] for i in range(top_k_internal)]
151
  kw_weight = np.zeros((len(topk_indices),)) + 0.1
152
 
153
  for k in query_kws:
 
163
  kw_weight = np.ones((len(topk_indices),))
164
 
165
  if self.weight_date == True:
166
+ sub_dates = [small_corpus['date'][i] for i in range(top_k_internal)]
167
  date = datetime.now().date()
168
  date_diff = np.array([((date - i).days / 365.) for i in sub_dates])
169
  # age_weight = (1 + np.exp(date_diff/2.1))**(-1) + 0.5
 
174
 
175
  if self.weight_citation == True:
176
  # st.write('weighting by citations')
177
+ sub_cites = np.array([small_corpus['cites'][i] for i in range(top_k_internal)])
178
  temp = sub_cites.copy()
179
  temp[sub_cites > 300] = 300.
180
  cite_weight = (1 + np.exp((300-temp)/42.0))**(-1.)
 
187
  filtered_results = [[topk_indices[i], similarities[i]] for i in range(len(similarities))]
188
  top_results = sorted(filtered_results, key=lambda x: x[1], reverse=True)[:top_k]
189
 
190
+ top_scores = [doc[1] for doc in top_results]
191
+ top_indices = [doc[0] for doc in top_results]
192
+ small_df = self.dataset[top_indices]
193
+
194
  if return_scores:
195
+ return {doc[0]: doc[1] for doc in top_results}, small_df
196
 
197
  # Only keep the document IDs
198
  top_results = [doc[0] for doc in top_results]
199
+ return top_results, small_df
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
200
 
201
  def generate_doc(self, query: str):
202
  prompt = """You are an expert astronomer. Given a scientific query, generate the abstract of an expert-level research paper
203
  that answers the question. Stick to a maximum length of {} tokens and return just the text of the abstract and conclusion.
204
  Do not include labels for any section. Use research-specific jargon.""".format(self.max_doclen)
 
 
 
 
 
 
 
 
 
 
 
205
 
206
  messages = [("system",prompt,),("human", query),]
207
+ return self.hyde_client.invoke(messages).content
 
 
208
 
209
  def generate_docs(self, query: str):
210
  docs = []
 
215
  def embed_docs(self, docs: List[str]):
216
  return self.embed_batch(docs)
217
 
218
+ def retrieve(self, query, top_k, return_scores = False,
219
+ embed_query=True, max_doclen=250,
220
+ generate_n=1, temperature=0.5,
221
+ rerank_top_k = 250):
222
 
223
+ if max_doclen * generate_n > 8191:
224
+ raise ValueError("Too many tokens. Please reduce max_doclen or generate_n.")
225
 
226
+ query_embedding = self.get_query_embedding(query)
 
 
 
 
227
 
228
+ if self.hyde == True:
229
+ self.max_doclen = max_doclen
230
+ self.generate_n = generate_n
231
+ self.hyde_client.temperature = temperature
232
+ self.embed_query = embed_query
233
+ docs = self.generate_docs(query)
234
+ st.expander('Abstract generated with hyde', expanded=False).write(docs)
235
+ doc_embeddings = self.embed_docs(docs)
236
+ if self.embed_query:
237
+ query_emb = self.embed_docs([query])[0]
238
+ doc_embeddings.append(query_emb)
239
+ query_embedding = np.mean(np.array(doc_embeddings), axis = 0)
240
+
241
+ if self.rerank == True:
242
+ top_results, small_df = self.rank_and_filter(query,
243
+ query_embedding,
244
+ rerank_top_k,
245
+ return_scores = False)
246
+ try:
247
+ docs_for_rerank = [small_df['abstract'][i] for i in range(rerank_top_k)]
248
+ if len(docs_for_rerank) == 0:
249
+ return []
250
+ reranked_results = self.cohere_client.rerank(
251
+ query=query,
252
+ documents=docs_for_rerank,
253
+ model='rerank-english-v3.0',
254
+ top_n=top_k
255
+ )
256
+ final_results = []
257
+ for result in reranked_results.results:
258
+ doc_id = top_results[result.index]
259
+ doc_text = docs_for_rerank[result.index]
260
+ score = float(result.relevance_score)
261
+ final_results.append([doc_id, "", score])
262
+ final_indices = [doc[0] for doc in final_results]
263
+ if return_scores:
264
+ return {result[0]: result[2] for result in final_results}, self.dataset[final_indices]
265
+ return [doc[0] for doc in final_results], self.dataset[final_indices]
266
+ except:
267
+ print('heavy load, please wait 10s and try again.')
268
+ else:
269
+ top_results, small_df = self.rank_and_filter(query,
270
+ query_embedding,
271
+ top_k,
272
+ return_scores = return_scores)
273
 
274
+ return top_results, small_df
275
 
276
+ def return_formatted_df(self, top_results, small_df):
 
 
277
 
278
+ df = pd.DataFrame(small_df)
279
+ df = df.drop(columns=['embed','umap_x','umap_y','cite_bibcodes','ref_bibcodes'])
280
+ links = ['https://ui.adsabs.harvard.edu/abs/'+i+'/abstract' for i in small_df['bibcode']]
281
+ scores = [top_results[i] for i in top_results]
282
+ df.insert(1,'ADS Link',links,True)
283
+ df.insert(2,'Relevance',scores,True)
284
+ df = df[['ADS Link','Relevance','date','cites','title','authors','abstract','keywords','ads_id']]
285
+ return df
286
 
287
+ # @st.cache_resource
288
+ def load_ret_system():
289
+ with st.spinner('loading retrieval system...'):
290
+ ec = RetrievalSystem()
291
+ st.toast('loaded retrieval system. time taken: %.2f sec' %(time.time()-ts))
292
+ return ec
293
+
294
+
295
+ st.image('local_files/pathfinder_logo.png')
296
 
297
+ st.expander("What is Pathfinder / How do I use it?", expanded=False).write(
298
+ """
299
+ Pathfinder v2.0 is a framework for searching and visualizing astronomy papers on the [arXiv](https://arxiv.org/) and [ADS](https://ui.adsabs.harvard.edu/) using the context
300
+ sensitivity from modern large language models (LLMs) to better parse patterns in paper contexts.
 
 
301
 
302
+ This tool was built during the [JSALT workshop](https://www.clsp.jhu.edu/2024-jelinek-summer-workshop-on-speech-and-language-technology/) to do awesome things.
 
 
303
 
304
+ **πŸ‘ˆ Use the sidebar to tweak the search parameters to get better results**.
305
 
306
+ ### Tool summary:
307
+ - Please wait while the initial data loads and compiles, this takes about a minute initially.
308
 
309
+ This is not meant to be a replacement to existing tools like the
310
+ [ADS](https://ui.adsabs.harvard.edu/),
311
+ [arxivsorter](https://www.arxivsorter.org/), semantic search or google scholar, but rather a supplement to find papers
312
+ that otherwise might be missed during a literature survey.
313
+ It is trained on astro-ph (astrophysics of galaxies) papers up to last-year-ish mined from arxiv and supplemented with ADS metadata,
314
+ if you are interested in extending it please reach out!
315
 
316
+ Also add: feedback form, socials, literature, contact us, copyright, collaboration, etc.
317
+
318
+ The image below shows a representation of all the astro-ph.GA papers that can be explored in more detail
319
+ using the `Arxiv embedding` page. The papers tend to cluster together by similarity, and result in an
320
+ atlas that shows well studied (forests) and currently uncharted areas (water).
321
+ """
322
+ )
323
+
324
+ st.sidebar.header("Fine-tune the search")
325
+ top_k = st.sidebar.slider("Number of papers to retrieve:", 1, 30, 10)
326
+ extra_keywords = st.sidebar.text_input("Enter extra keywords (comma-separated):")
327
+ keywords = [kw.strip() for kw in extra_keywords.split(',')] if extra_keywords else []
328
 
329
+ st.sidebar.subheader("Toggles")
330
+ toggle_a = st.sidebar.toggle("Weight by keywords", value = False)
331
+ toggle_b = st.sidebar.toggle("Weight by date", value = False)
332
+ toggle_c = st.sidebar.toggle("Weight by citations", value = False)
333
+ toggles = {'Keyword weighting': toggle_a, 'Time weighting': toggle_b, 'Citation weighting': toggle_c}
334
+
335
+ method = st.sidebar.radio("Retrieval method:", ["Semantic search", "Semantic search + HyDE", "Semantic search + HyDE + CoHERE"], index=2)
336
+ method2 = st.sidebar.radio("Generation complexity:", ["Basic RAG","ReAct Agent"])
337
 
338
+ st.session_state.top_k = top_k
339
+ st.session_state.keywords = keywords
340
+ st.session_state.toggles = toggles
341
+ st.session_state.method = method
342
+ st.session_state.method2 = method2
343
+
344
+ if (method == "Semantic search"):
345
+ st.session_state.hyde = False
346
+ st.session_state.cohere = False
347
+ elif (method == "Semantic search + HyDE"):
348
+ st.session_state.hyde = True
349
+ st.session_state.cohere = False
350
+ elif (method == "Semantic search + HyDE + CoHERE"):
351
+ st.session_state.hyde = True
352
+ st.session_state.cohere = True
353
+
354
+ if method2 == "Basic RAG":
355
+ st.session_state.gen_method = 'rag'
356
+ elif method2 == "ReAct Agent":
357
+ st.session_state.gen_method = 'agent'
358
+
359
+ question_type = st.sidebar.selectbox("Prompt specialization:", ["Multi-paper (Default)", "Single-paper", "Bibliometric", "Broad but nuanced"])
360
+ st.session_state.question_type = question_type
361
+ # store_output = st.sidebar.button("Save output")
362
+
363
+ query = st.text_input("Ask me anything:")
364
+ st.session_state.query = query
365
+ st.write(query)
366
+ submit_button = st.button("Run pathfinder!", key='runpfdr')
367
 
368
+ search_text_list = ['rooting around in the paper pile...','looking for clarity...','scanning the event horizon...','peering into the abyss...','potatoes power this ongoing search...']
369
+ gen_text_list = ['making the LLM talk to the papers...','invoking arcane rituals...','gone to library, please wait...','is there really an answer to this...']
370
+
371
+ if 'arxiv_corpus' not in st.session_state:
372
+ st.session_state.arxiv_corpus = load_arxiv_corpus()
373
+
374
+ # @st.fragment()
375
+ def run_query_ret(query):
376
+ tr = time.time()
377
+ ec = load_ret_system()
378
+ ec.query_input_keywords = st.session_state.keywords
379
+ ec.toggles = st.session_state.toggles
380
+ ec.hyde = st.session_state.hyde
381
+ ec.rerank = st.session_state.cohere
382
+ rs, small_df = ec.retrieve(query, top_k = st.session_state.top_k, return_scores=True)
383
+ formatted_df = ec.return_formatted_df(rs, small_df)
384
+ st.toast('got top-k papers. time taken: %.2f sec' %(time.time()-tr))
385
+ return formatted_df
386
+
387
+ def Library(query):
388
+ papers_df = run_query_ret(st.session_state.query)
389
  op_docs = ''
390
+ for i in range(len(papers_df)):
391
+ op_docs = op_docs + 'Paper %.0f:' %(i+1) + papers_df['title'][i] + '\n' + papers_df['abstract'][i] + '\n\n'
392
 
393
  return op_docs
394
 
395
+ def run_agent_qa(query):
396
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
397
  search = DuckDuckGoSearchAPIWrapper()
398
  tools = [
399
  Tool(
 
411
  if 'tools' not in st.session_state:
412
  st.session_state.tools = tools
413
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
414
  prompt = hub.pull("hwchase17/react")
415
+ prompt.template = react_prompt
 
 
416
 
417
  file_path = "agent_trace.txt"
418
  try:
 
422
  file_handler = FileCallbackHandler(file_path)
423
  callback_manager=CallbackManager([file_handler])
424
 
 
 
425
  tool_names = [tool.name for tool in st.session_state.tools]
426
  if 'agent' not in st.session_state:
427
  # agent = ZeroShotAgent(llm_chain=llm_chain, allowed_tools=tool_names)
 
435
  answer = st.session_state.agent_executor.invoke({"input": query,})
436
  return answer
437
 
438
+ def run_rag_qa(query, papers_df):
 
 
 
439
 
440
+ try:
441
+ loaders = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
442
 
443
+ documents = []
444
+ my_bar = st.progress(0, text='adding documents to LLM context')
445
 
446
+ for i, row in papers_df.iterrows():
447
+ content = f"Paper {i+1}: {row['title']}\n{row['abstract']}\n\n"
448
+ metadata = {"source": row['ads_id']}
449
+ doc = Document(page_content=content, metadata=metadata)
450
+ documents.append(doc)
451
+ my_bar.progress((i+1)/len(papers_df), text='adding documents to LLM context')
452
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
453
  text_splitter = RecursiveCharacterTextSplitter(chunk_size=150, chunk_overlap=50, add_start_index=True)
454
+
455
+ splits = text_splitter.split_documents(documents)
456
  vectorstore = Chroma.from_documents(documents=splits, embedding=st.session_state.embeddings, collection_name='retdoc4')
457
  # retriever = vectorstore.as_retriever(search_type="similarity", search_kwargs={"k": 6, "fetch_k": len(splits)})
458
  retriever = vectorstore.as_retriever(search_type="similarity", search_kwargs={"k": 6})
 
 
 
 
 
 
459
 
460
  if st.session_state.question_type == 'Bibliometric':
461
  template = bibliometric_prompt
 
466
  else:
467
  template = regular_prompt
468
  prompt = PromptTemplate.from_template(template)
469
+
470
  def format_docs(docs):
471
  return "\n\n".join(doc.page_content for doc in docs)
472
+
 
473
  rag_chain_from_docs = (
474
  RunnablePassthrough.assign(context=(lambda x: format_docs(x["context"])))
475
  | prompt
476
  | st.session_state.gen_llm
477
  | StrOutputParser()
478
  )
479
+
480
  rag_chain_with_source = RunnableParallel(
481
  {"context": retriever, "question": RunnablePassthrough()}
482
  ).assign(answer=rag_chain_from_docs)
 
483
  rag_answer = rag_chain_with_source.invoke(query, )
 
484
  vectorstore.delete_collection()
485
 
486
+ except:
487
+ st.subheader('heavy load! please wait 10 seconds and try again.')
488
+
489
+ return rag_answer
490
 
491
  def guess_question_type(query: str):
492
+
493
+ gen_client = openai_llm(temperature=0,model_name='gpt-4o-mini', openai_api_key = openai_key)
494
+ messages = [("system",question_categorization_prompt,),("human", query),]
495
+ return gen_client.invoke(messages).content
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
496
 
497
  class OverallConsensusEvaluation(BaseModel):
498
  consensus: Literal["Strong Agreement", "Moderate Agreement", "Weak Agreement", "No Clear Consensus", "Weak Disagreement", "Moderate Disagreement", "Strong Disagreement"] = Field(
 
547
 
548
  return response
549
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
550
 
 
 
 
551
 
 
552
 
553
+ # ---------------------------------------
554
 
555
+ if st.session_state.get('runpfdr'):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
556
  with st.spinner(search_text_list[np.random.choice(len(search_text_list))]):
557
+ st.write('Settings: [Kw:',toggle_a, 'Time:',toggle_b, 'Cite:',toggle_c, '] top_k:',top_k, 'retrieval:',method)
558
 
559
+ papers_df = run_query_ret(st.session_state.query)
560
+ st.header(st.session_state.query)
561
+ st.subheader('top-k relevant papers:')
562
+ st.data_editor(papers_df, column_config = {'ADS Link':st.column_config.LinkColumn(display_text= 'https://ui.adsabs.harvard.edu/abs/(.*?)/abstract')})
 
 
 
 
 
 
563
 
564
+ with st.spinner(gen_text_list[np.random.choice(len(gen_text_list))]):
 
 
565
 
566
+ if st.session_state.gen_method == 'agent':
567
+ answer = run_agent_qa(st.session_state.query)
568
  answer_text = answer['output']
569
+ st.subheader('Answer with '+method2)
570
  st.write(answer_text)
 
571
  file_path = "agent_trace.txt"
572
  with open(file_path, 'r') as file:
573
  intermediate_steps = file.read()
574
  st.expander('Intermediate steps', expanded=False).write(intermediate_steps)
575
 
576
  elif st.session_state.gen_method == 'rag':
577
+ answer = run_rag_qa(query, papers_df)
578
+ st.subheader('Answer with '+method2)
579
  answer_text = answer['answer']
580
  st.write(answer_text)
581
 
582
+ query_kws = get_keywords(query)
583
+ input_kws = st.session_state.keywords
584
+ query_kws = query_kws + input_kws
585
+ triggered_keywords = query_kws + input_kws
 
 
 
 
 
586
  st.write('**Triggered keywords:** `'+ "`, `".join(triggered_keywords)+'`')
587
+
588
  col1, col2 = st.columns(2)
589
 
590
  with col1:
591
+ with st.spinner("Evaluating question type"):
592
+ with st.expander("Question type", expanded=True):
593
+ st.subheader("Question type suggestion")
594
+ question_type_gen = guess_question_type(query)
595
+ if '<categorization>' in question_type_gen:
596
+ question_type_gen = question_type_gen.split('<categorization>')[1]
597
+ if '</categorization>' in question_type_gen:
598
+ question_type_gen = question_type_gen.split('</categorization>')[0]
599
+ question_type_gen = question_type_gen.replace('\n',' \n')
600
+ st.markdown(question_type_gen)
601
 
602
  with col2:
603
+ with st.spinner("Evaluating abstract consensus"):
604
+ with st.expander("Abstract consensus", expanded=True):
605
+ consensus_answer = evaluate_overall_consensus(query, [papers_df['abstract'][i] for i in range(len(papers_df))])
606
+ st.subheader("Consensus: "+consensus_answer.consensus)
607
+ st.markdown(consensus_answer.explanation)
608
+ st.markdown('Relevance of retrieved papers to answer: %.1f' %consensus_answer.relevance_score)
609
+
610
  session_vars = {
611
  "runtime": "pathfinder_v1_online",
612
  "query": query,
613
  "question_type": question_type,
614
+ 'Keyword weighting': toggle_a,
615
+ 'Time weighting': toggle_b,
616
  'Citation weighting': toggle_c,
617
  "rag_method" : method,
618
  "gen_method" : method2,
619
  "answer" : answer_text,
620
+ "question_type": question_type_gen,
621
+ "consensus": consensus_answer.explanation,
622
+ "topk" : list(papers_df['ads_id']),
623
+ "topk_scores" : list(papers_df['Relevance']),
624
  "topk_papers": list(papers_df['ADS Link']),
625
  }
626
+
627
  @st.fragment()
628
  def download_op(data):
629
  json_string = json.dumps(data)
 
632
  file_name="pathfinder_data.json",
633
  mime="application/json",
634
  data=json_string,)
635
+
636
+ # with st.sidebar:
637
+ download_op(session_vars)
638
+
 
 
 
639
  else:
640
  st.info("Use the sidebar to tweak the search parameters to get better results.")