yogl commited on
Commit
4df857a
·
verified ·
1 Parent(s): d6c575d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +150 -27
app.py CHANGED
@@ -2,7 +2,8 @@ import os
2
  import io
3
  import json
4
  import uuid
5
- from typing import Optional, Tuple, List
 
6
  from datetime import datetime, timezone
7
 
8
  import numpy as np
@@ -24,10 +25,13 @@ HF_MERGED_REPO = os.getenv("HF_MERGED_REPO")
24
  HF_EMB_REPO = os.getenv("HF_EMB_REPO")
25
  MODEL_NAME = os.getenv("MODEL_NAME")
26
 
27
- # Репозиторий для заявок (вы создали его вручную)
28
  HF_REQUESTS_REPO = os.getenv("HF_REQUESTS_REPO", "PlanetExpress2125/PostDocRequests")
29
  HF_REQUESTS_REPO_TYPE = os.getenv("HF_REQUESTS_REPO_TYPE", "dataset") # dataset | model
30
- HF_WRITE_TOKEN = os.getenv("HF_WRITE_TOKEN") # должен быть write; если не задан — попробуем HF_TOKEN
 
 
 
31
 
32
  SLIDER_MIN_YEAR = 2005 # нижняя граница диапазона лет
33
 
@@ -41,6 +45,7 @@ COLUMN_LABELS_RU_EXCEL = {
41
  "protection_year": "Год",
42
  "registration_number": "Регистрационный номер",
43
  "vak_link": "Ссылка ВАК",
 
44
  }
45
 
46
  DISPLAY_COLUMNS_ALL = [
@@ -144,11 +149,11 @@ def _keyify(label: str) -> str:
144
  def save_request_to_hub(payload: dict) -> str:
145
  """
146
  Сохраняем каждую заявку отдельным JSON-файлом в:
147
- PlanetExpress2125/PostDocRequests/requests/<timestamp>_<uuid>.json
148
 
149
  Требуется токен с правами write:
150
  - HF_WRITE_TOKEN (рекомендуется)
151
- - если не задан, будет использован HF_TOKEN
152
  """
153
  token = HF_WRITE_TOKEN or HF_TOKEN
154
  if not token:
@@ -172,6 +177,109 @@ def save_request_to_hub(payload: dict) -> str:
172
  return path_in_repo
173
 
174
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
  # ==========================
176
  # ЗАГРУЗКА ДАННЫХ И МОДЕЛИ
177
  # ==========================
@@ -355,7 +463,7 @@ def build_result_df(results):
355
  if not rows:
356
  return pd.DataFrame(columns=DISPLAY_COLUMNS_ALL + ["vak_link"])
357
 
358
- df_res = pd.DataFrame(rows)
359
  return df_res[DISPLAY_COLUMNS_ALL + ["vak_link"]]
360
 
361
 
@@ -371,8 +479,14 @@ def run_search(
371
  results = search_core(query, top_k, mask=mask)
372
  df_raw = build_result_df(results)
373
 
 
 
 
 
 
 
374
  # UI: скрываем №, reg номер, тип
375
- df_ui = df_raw.copy()
376
  df_ui = df_ui.rename(
377
  columns={
378
  "score": "Сходство",
@@ -381,12 +495,15 @@ def run_search(
381
  "author_org_short": "Организация",
382
  "protection_year": "Год",
383
  "vak_link": "ВАК",
 
384
  }
385
  )
386
  df_ui = df_ui.drop(columns=["№", "registration_number", "dissertation_type"])
387
 
388
- # Excel: полный набор
389
- df_excel_ru = df_raw.rename(columns=COLUMN_LABELS_RU_EXCEL)
 
 
390
  output = io.BytesIO()
391
  with pd.ExcelWriter(output, engine="xlsxwriter") as writer:
392
  df_excel_ru.to_excel(writer, index=False)
@@ -398,7 +515,7 @@ def run_search(
398
  def format_selected_list_for_ui(df_ui_show: pd.DataFrame, selected_idx: List[int]) -> str:
399
  lines = []
400
  for i in selected_idx:
401
- r = df_ui_show.loc[i]
402
  fio = str(r.get("ФИО", "")).strip()
403
  title = str(r.get("Название диссертации", "")).strip()
404
  year = r.get("Год", "")
@@ -501,15 +618,16 @@ excel_saved = st.session_state.last_excel
501
  if isinstance(df_ui_saved, pd.DataFrame) and not df_ui_saved.empty:
502
  st.success(f"Найдено записей: {len(df_ui_saved)}")
503
 
504
- # Важно: сохраняем индекс, чтобы выбор строк корректно мапился обратно в df_raw_saved через .loc
505
- df_ui_show = df_ui_saved[["Сходство", "ФИО", "Название диссертации", "Организация", "Год", "ВАК"]].copy()
 
506
 
507
  # Таблица: сортировка по клику по заголовку + мультивыбор строк (чекбоксы)
508
  event = st.dataframe(
509
  df_ui_show,
510
  use_container_width=True,
511
  hide_index=True,
512
- column_order=["Сходство", "ФИО", "Название диссертации", "Организация", "Год", "ВАК"],
513
  column_config={
514
  "Сходство": st.column_config.NumberColumn("Сходство", format="%.4f", width="small"),
515
  "ФИО": st.column_config.TextColumn("ФИО", width="medium"),
@@ -517,15 +635,21 @@ if isinstance(df_ui_saved, pd.DataFrame) and not df_ui_saved.empty:
517
  "Организация": st.column_config.TextColumn("Организация", width="medium"),
518
  "Год": st.column_config.NumberColumn("Год", width="small"),
519
  "ВАК": st.column_config.LinkColumn("ВАК", display_text="открыть", width="small"),
 
 
 
 
 
 
 
520
  },
521
  on_select="rerun",
522
  selection_mode="multi-row",
523
  )
524
 
525
- # сохраняем выбранные индексы
526
  sel = getattr(event, "selection", None)
527
  if sel is not None and sel.rows is not None:
528
- # sel.rows — индексы строк (не обязательно 0..N после сортировки в UI)
529
  st.session_state.selected_rows = list(sel.rows)
530
 
531
  if excel_saved is not None:
@@ -536,7 +660,7 @@ if isinstance(df_ui_saved, pd.DataFrame) and not df_ui_saved.empty:
536
  mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
537
  )
538
 
539
- # ======= ЗАПРОС РАСШИРЕННОЙ ИНФОРМАЦИИ (ЗАГЛУШКА: сохраняем в PostDocRequests) =======
540
  st.markdown("---")
541
  st.subheader("Запрос расширенной информации")
542
 
@@ -554,8 +678,11 @@ if isinstance(df_ui_saved, pd.DataFrame) and not df_ui_saved.empty:
554
 
555
  comment = st.text_area(
556
  "Комментарий",
557
- height=120,
558
- placeholder="Что именно вы хотите уточнить/получить по выбранным диссертациям?",
 
 
 
559
  )
560
 
561
  send_request = st.form_submit_button("📨 Отправить запрос", type="primary", use_container_width=True)
@@ -566,10 +693,10 @@ if isinstance(df_ui_saved, pd.DataFrame) and not df_ui_saved.empty:
566
  elif len(selected_rows) == 0:
567
  st.warning("Выберите хотя бы одну диссертацию в таблице (чекбоксами слева).")
568
  else:
569
- # Собираем items по выбранным индексам через df_raw_saved.loc
570
  items = []
571
- for idx in selected_rows:
572
- raw = df_raw_saved.loc[idx].to_dict()
573
  items.append(
574
  {
575
  "author_fio": raw.get("fio"),
@@ -579,6 +706,7 @@ if isinstance(df_ui_saved, pd.DataFrame) and not df_ui_saved.empty:
579
  "vak_link": raw.get("vak_link"),
580
  "registration_number": raw.get("registration_number"),
581
  "score": raw.get("score"),
 
582
  }
583
  )
584
 
@@ -595,12 +723,7 @@ if isinstance(df_ui_saved, pd.DataFrame) and not df_ui_saved.empty:
595
 
596
  try:
597
  path = save_request_to_hub(payload)
598
- st.success(f"Запрос сохранён в {HF_REQUESTS_REPO}: {path}")
599
-
600
- # При желании можно показать, что ушло (на время отладки)
601
- with st.expander("Показать содержимое заявки (для проверки)", expanded=False):
602
- st.json(payload)
603
-
604
  except Exception as e:
605
  st.error(
606
  "Не удалось сохранить запрос в репозиторий PostDocRequests.\n\n"
 
2
  import io
3
  import json
4
  import uuid
5
+ import re
6
+ from typing import Optional, Tuple, List, Dict, Any
7
  from datetime import datetime, timezone
8
 
9
  import numpy as np
 
25
  HF_EMB_REPO = os.getenv("HF_EMB_REPO")
26
  MODEL_NAME = os.getenv("MODEL_NAME")
27
 
28
+ # Репозиторий для заявок
29
  HF_REQUESTS_REPO = os.getenv("HF_REQUESTS_REPO", "PlanetExpress2125/PostDocRequests")
30
  HF_REQUESTS_REPO_TYPE = os.getenv("HF_REQUESTS_REPO_TYPE", "dataset") # dataset | model
31
+ HF_WRITE_TOKEN = os.getenv("HF_WRITE_TOKEN") # write token екомендуется)
32
+
33
+ # OpenAlex index dataset
34
+ OA_INDEX_REPO = os.getenv("OA_INDEX_REPO", "yogl/diss_authors_with_oa_index")
35
 
36
  SLIDER_MIN_YEAR = 2005 # нижняя граница диапазона лет
37
 
 
45
  "protection_year": "Год",
46
  "registration_number": "Регистрационный номер",
47
  "vak_link": "Ссылка ВАК",
48
+ "openalex_url": "OpenAlex",
49
  }
50
 
51
  DISPLAY_COLUMNS_ALL = [
 
149
  def save_request_to_hub(payload: dict) -> str:
150
  """
151
  Сохраняем каждую заявку отдельным JSON-файлом в:
152
+ <HF_REQUESTS_REPO>/requests/<timestamp>_<uuid>.json
153
 
154
  Требуется токен с правами write:
155
  - HF_WRITE_TOKEN (рекомендуется)
156
+ - если не задан, будет использован HF_TOKEN (если он write)
157
  """
158
  token = HF_WRITE_TOKEN or HF_TOKEN
159
  if not token:
 
177
  return path_in_repo
178
 
179
 
180
+ # ==========================
181
+ # OPENALEX INDEX
182
+ # ==========================
183
+
184
+ def _normalize_regnum(x: Any) -> Optional[str]:
185
+ if x is None:
186
+ return None
187
+ s = str(x).strip()
188
+ if not s or s.lower() == "nan":
189
+ return None
190
+ return s
191
+
192
+
193
+ def _iter_registration_numbers(value: Any) -> List[str]:
194
+ """
195
+ registration_numbers может быть:
196
+ - list/tuple
197
+ - строкой (один номер или несколько через , ; пробел)
198
+ - строкой вида "['123','456']"
199
+ """
200
+ if value is None:
201
+ return []
202
+ if isinstance(value, (list, tuple, np.ndarray)):
203
+ out = []
204
+ for v in value:
205
+ r = _normalize_regnum(v)
206
+ if r:
207
+ out.append(r)
208
+ return out
209
+
210
+ s = str(value).strip()
211
+ if not s or s.lower() == "nan":
212
+ return []
213
+
214
+ # попытка распарсить список
215
+ if (s.startswith("[") and s.endswith("]")) or (s.startswith("(") and s.endswith(")")):
216
+ try:
217
+ parsed = json.loads(s)
218
+ if isinstance(parsed, list):
219
+ return [r for r in (_normalize_regnum(v) for v in parsed) if r]
220
+ except Exception:
221
+ pass
222
+
223
+ # fallback: split
224
+ parts = re.split(r"[,\s;]+", s)
225
+ return [r for r in (_normalize_regnum(p) for p in parts) if r]
226
+
227
+
228
+ def _normalize_oa_url(value: Any) -> str:
229
+ if value is None:
230
+ return ""
231
+ s = str(value).strip()
232
+ if not s or s.lower() == "nan":
233
+ return ""
234
+ # если уже URL
235
+ if s.startswith("http://") or s.startswith("https://"):
236
+ # нормализуем домен/слеши
237
+ m = re.search(r"(A\d+)", s)
238
+ if m:
239
+ return f"https://openalex.org/{m.group(1)}"
240
+ return s
241
+ # если только ID
242
+ m = re.match(r"^A\d+$", s)
243
+ if m:
244
+ return f"https://openalex.org/{s}"
245
+ # если вдруг мусор — попробуем вытащить A...
246
+ m = re.search(r"(A\d+)", s)
247
+ if m:
248
+ return f"https://openalex.org/{m.group(1)}"
249
+ return ""
250
+
251
+
252
+ @st.cache_data(show_spinner="Загрузка индекса OpenAlex...")
253
+ def load_openalex_map() -> Dict[str, str]:
254
+ """
255
+ Строим маппинг: registration_number -> openalex_url
256
+ из yogl/diss_authors_with_oa_index (поле registration_numbers + oa_author_id).
257
+ """
258
+ try:
259
+ ds = load_dataset(OA_INDEX_REPO, split="train")
260
+ df = ds.to_pandas()
261
+ except Exception:
262
+ return {}
263
+
264
+ if "registration_numbers" not in df.columns or "oa_author_id" not in df.columns:
265
+ return {}
266
+
267
+ mapping: Dict[str, str] = {}
268
+ for _, row in df.iterrows():
269
+ regs = row.get("registration_numbers", None)
270
+ oa = row.get("oa_author_id", None)
271
+ url = _normalize_oa_url(oa)
272
+ if not url:
273
+ continue
274
+ for reg in _iter_registration_numbers(regs):
275
+ if reg and reg not in mapping:
276
+ mapping[reg] = url
277
+ return mapping
278
+
279
+
280
+ OA_MAP = load_openalex_map()
281
+
282
+
283
  # ==========================
284
  # ЗАГРУЗКА ДАННЫХ И МОДЕЛИ
285
  # ==========================
 
463
  if not rows:
464
  return pd.DataFrame(columns=DISPLAY_COLUMNS_ALL + ["vak_link"])
465
 
466
+ df_res = pd.DataFrame(rows).reset_index(drop=True)
467
  return df_res[DISPLAY_COLUMNS_ALL + ["vak_link"]]
468
 
469
 
 
479
  results = search_core(query, top_k, mask=mask)
480
  df_raw = build_result_df(results)
481
 
482
+ # OpenAlex URL по registration_number
483
+ if not df_raw.empty and OA_MAP:
484
+ df_raw["openalex_url"] = df_raw["registration_number"].astype(str).map(OA_MAP).fillna("")
485
+ else:
486
+ df_raw["openalex_url"] = ""
487
+
488
  # UI: скрываем №, reg номер, тип
489
+ df_ui = df_raw.copy().reset_index(drop=True)
490
  df_ui = df_ui.rename(
491
  columns={
492
  "score": "Сходство",
 
495
  "author_org_short": "Организация",
496
  "protection_year": "Год",
497
  "vak_link": "ВАК",
498
+ "openalex_url": "OpenAlex",
499
  }
500
  )
501
  df_ui = df_ui.drop(columns=["№", "registration_number", "dissertation_type"])
502
 
503
+ # Excel: полный набор + OpenAlex
504
+ df_excel = df_raw.copy()
505
+ df_excel_ru = df_excel.rename(columns=COLUMN_LABELS_RU_EXCEL)
506
+
507
  output = io.BytesIO()
508
  with pd.ExcelWriter(output, engine="xlsxwriter") as writer:
509
  df_excel_ru.to_excel(writer, index=False)
 
515
  def format_selected_list_for_ui(df_ui_show: pd.DataFrame, selected_idx: List[int]) -> str:
516
  lines = []
517
  for i in selected_idx:
518
+ r = df_ui_show.iloc[i]
519
  fio = str(r.get("ФИО", "")).strip()
520
  title = str(r.get("Название диссертации", "")).strip()
521
  year = r.get("Год", "")
 
618
  if isinstance(df_ui_saved, pd.DataFrame) and not df_ui_saved.empty:
619
  st.success(f"Найдено записей: {len(df_ui_saved)}")
620
 
621
+ df_ui_show = df_ui_saved[
622
+ ["Сходство", "ФИО", "Название диссертации", "Организация", "Год", "ВАК", "OpenAlex"]
623
+ ].copy()
624
 
625
  # Таблица: сортировка по клику по заголовку + мультивыбор строк (чекбоксы)
626
  event = st.dataframe(
627
  df_ui_show,
628
  use_container_width=True,
629
  hide_index=True,
630
+ column_order=["Сходство", "ФИО", "Название диссертации", "Организация", "Год", "ВАК", "OpenAlex"],
631
  column_config={
632
  "Сходство": st.column_config.NumberColumn("Сходство", format="%.4f", width="small"),
633
  "ФИО": st.column_config.TextColumn("ФИО", width="medium"),
 
635
  "Организация": st.column_config.TextColumn("Организация", width="medium"),
636
  "Год": st.column_config.NumberColumn("Год", width="small"),
637
  "ВАК": st.column_config.LinkColumn("ВАК", display_text="открыть", width="small"),
638
+ # Отображаем ID (A123...) из URL через regex capture group
639
+ "OpenAlex": st.column_config.LinkColumn(
640
+ "OpenAlex",
641
+ display_text=r"https://openalex\.org/(A\d+)",
642
+ width="small",
643
+ help="Профиль автора в OpenAlex (если найден)",
644
+ ),
645
  },
646
  on_select="rerun",
647
  selection_mode="multi-row",
648
  )
649
 
650
+ # сохраняем выбранные строки (позиции 0..N-1 исходного df_ui_show)
651
  sel = getattr(event, "selection", None)
652
  if sel is not None and sel.rows is not None:
 
653
  st.session_state.selected_rows = list(sel.rows)
654
 
655
  if excel_saved is not None:
 
660
  mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
661
  )
662
 
663
+ # ======= ЗАПРОС РАСШИРЕННОЙ ИНФОРМАЦИИ (ЗАПИСЬ В PostDocRequests) =======
664
  st.markdown("---")
665
  st.subheader("Запрос расширенной информации")
666
 
 
678
 
679
  comment = st.text_area(
680
  "Комментарий",
681
+ height=140,
682
+ placeholder=(
683
+ "Какую дополнительную информацию по авторам диссертаций вы хотите получить?\n"
684
+ "Какие замечания/пожелания по функционалу системы?"
685
+ ),
686
  )
687
 
688
  send_request = st.form_submit_button("📨 Отправить запрос", type="primary", use_container_width=True)
 
693
  elif len(selected_rows) == 0:
694
  st.warning("Выберите хотя бы одну диссертацию в таблице (чекбоксами слева).")
695
  else:
696
+ # items по выбранным строкам
697
  items = []
698
+ for i in selected_rows:
699
+ raw = df_raw_saved.iloc[i].to_dict()
700
  items.append(
701
  {
702
  "author_fio": raw.get("fio"),
 
706
  "vak_link": raw.get("vak_link"),
707
  "registration_number": raw.get("registration_number"),
708
  "score": raw.get("score"),
709
+ "openalex_url": raw.get("openalex_url", ""),
710
  }
711
  )
712
 
 
723
 
724
  try:
725
  path = save_request_to_hub(payload)
726
+ st.success(f"Запрос {path} сохранен")
 
 
 
 
 
727
  except Exception as e:
728
  st.error(
729
  "Не удалось сохранить запрос в репозиторий PostDocRequests.\n\n"