BibexPy 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bibex_core/MergeDB.py +1189 -0
- bibex_core/__init__.py +13 -0
- bibex_core/modules/__init__.py +35 -0
- bibex_core/modules/api_utils.py +1172 -0
- bibex_core/modules/c1_utils.py +423 -0
- bibex_core/modules/file_utils.py +71 -0
- bibex_core/modules/ml_utils.py +332 -0
- bibex_core/modules/post_process.py +50 -0
- bibex_core/modules/stats_utils.py +216 -0
- bibex_core/scp2xlsx.py +235 -0
- bibex_core/wos2xlsx.py +159 -0
- bibex_core/xlsx2vos.py +185 -0
- bibexpy/__init__.py +9 -0
- bibexpy/__main__.py +6 -0
- bibexpy/_server/__init__.py +1 -0
- bibexpy/_server/config.py +92 -0
- bibexpy/_server/jobs/__init__.py +3 -0
- bibexpy/_server/jobs/runner.py +301 -0
- bibexpy/_server/main.py +211 -0
- bibexpy/_server/models/__init__.py +0 -0
- bibexpy/_server/models/project.py +25 -0
- bibexpy/_server/routers/__init__.py +0 -0
- bibexpy/_server/routers/audit.py +65 -0
- bibexpy/_server/routers/convert.py +101 -0
- bibexpy/_server/routers/disambiguate.py +177 -0
- bibexpy/_server/routers/downloads.py +26 -0
- bibexpy/_server/routers/enrich.py +42 -0
- bibexpy/_server/routers/export.py +116 -0
- bibexpy/_server/routers/export_folder.py +97 -0
- bibexpy/_server/routers/filter.py +131 -0
- bibexpy/_server/routers/jobs.py +72 -0
- bibexpy/_server/routers/merge.py +437 -0
- bibexpy/_server/routers/prepare.py +241 -0
- bibexpy/_server/routers/projects.py +49 -0
- bibexpy/_server/routers/quality.py +288 -0
- bibexpy/_server/routers/records.py +204 -0
- bibexpy/_server/routers/report.py +111 -0
- bibexpy/_server/routers/settings.py +383 -0
- bibexpy/_server/routers/system.py +209 -0
- bibexpy/_server/routers/tools.py +257 -0
- bibexpy/_server/routers/upload.py +110 -0
- bibexpy/_server/services/__init__.py +0 -0
- bibexpy/_server/services/analyses.py +364 -0
- bibexpy/_server/services/audit.py +340 -0
- bibexpy/_server/services/bibex_adapter.py +47 -0
- bibexpy/_server/services/bibtex_writer.py +104 -0
- bibexpy/_server/services/converter.py +205 -0
- bibexpy/_server/services/disambiguation/__init__.py +19 -0
- bibexpy/_server/services/disambiguation/blocking.py +581 -0
- bibexpy/_server/services/disambiguation/cache.py +47 -0
- bibexpy/_server/services/disambiguation/deepseek_client.py +203 -0
- bibexpy/_server/services/disambiguation/orcid.py +261 -0
- bibexpy/_server/services/disambiguation/pipeline.py +985 -0
- bibexpy/_server/services/disambiguation/similarity.py +91 -0
- bibexpy/_server/services/enricher.py +338 -0
- bibexpy/_server/services/exporter.py +112 -0
- bibexpy/_server/services/filter_engine.py +300 -0
- bibexpy/_server/services/merger.py +202 -0
- bibexpy/_server/services/methodology.py +102 -0
- bibexpy/_server/services/report_export.py +300 -0
- bibexpy/_server/services/ris_writer.py +62 -0
- bibexpy/_server/services/smart_merger.py +967 -0
- bibexpy/_server/services/storage.py +165 -0
- bibexpy/_web/404/index.html +1 -0
- bibexpy/_web/404.html +1 -0
- bibexpy/_web/_next/static/chunks/117-7ad5186126fa076d.js +2 -0
- bibexpy/_web/_next/static/chunks/156-75beffb8ae92a7f3.js +1 -0
- bibexpy/_web/_next/static/chunks/169-301c04fcec22686f.js +4 -0
- bibexpy/_web/_next/static/chunks/2-ecc1c36a5632ac61.js +1 -0
- bibexpy/_web/_next/static/chunks/244-be343e0f8aa00fb5.js +1 -0
- bibexpy/_web/_next/static/chunks/367-b7457c2b95f6932b.js +1 -0
- bibexpy/_web/_next/static/chunks/382-9cd163eed632e241.js +1 -0
- bibexpy/_web/_next/static/chunks/392-afa8bfa4c1a9893d.js +1 -0
- bibexpy/_web/_next/static/chunks/507-63607ded9f83c1aa.js +1 -0
- bibexpy/_web/_next/static/chunks/624-cae565632f47217d.js +1 -0
- bibexpy/_web/_next/static/chunks/739-a00c6b9c0f11813e.js +1 -0
- bibexpy/_web/_next/static/chunks/862-8e1bd5f93c082c02.js +1 -0
- bibexpy/_web/_next/static/chunks/app/_not-found/page-873ba59b5eb7c342.js +1 -0
- bibexpy/_web/_next/static/chunks/app/layout-0d5fc68ac8971a04.js +1 -0
- bibexpy/_web/_next/static/chunks/app/page-cf2bea5edc74459a.js +1 -0
- bibexpy/_web/_next/static/chunks/app/projects/[id]/convert/page-ab6dd645ae113b64.js +1 -0
- bibexpy/_web/_next/static/chunks/app/projects/[id]/disambiguate/page-dd98482cc47344e9.js +1 -0
- bibexpy/_web/_next/static/chunks/app/projects/[id]/enrich/page-4727e431468514e9.js +1 -0
- bibexpy/_web/_next/static/chunks/app/projects/[id]/export/page-f9005d164bb22cb0.js +1 -0
- bibexpy/_web/_next/static/chunks/app/projects/[id]/layout-07755fda55b3ae1f.js +1 -0
- bibexpy/_web/_next/static/chunks/app/projects/[id]/merge/page-45b95318ea7beccf.js +1 -0
- bibexpy/_web/_next/static/chunks/app/projects/[id]/records/page-bfad80457da9eb0b.js +1 -0
- bibexpy/_web/_next/static/chunks/app/projects/[id]/report/page-f05767e74158e995.js +1 -0
- bibexpy/_web/_next/static/chunks/app/projects/[id]/upload/page-8df78ca9cdecbcb8.js +1 -0
- bibexpy/_web/_next/static/chunks/app/projects/page-99960711ac6a13e9.js +1 -0
- bibexpy/_web/_next/static/chunks/app/settings/page-002df809d6c5a227.js +1 -0
- bibexpy/_web/_next/static/chunks/app/tools/page-27eee1c6a590859f.js +1 -0
- bibexpy/_web/_next/static/chunks/fd9d1056-f2718be92181f426.js +1 -0
- bibexpy/_web/_next/static/chunks/framework-f66176bb897dc684.js +1 -0
- bibexpy/_web/_next/static/chunks/main-app-d138a6e2f8c847f6.js +1 -0
- bibexpy/_web/_next/static/chunks/main-c4b9ddbf514503ef.js +1 -0
- bibexpy/_web/_next/static/chunks/pages/_app-72b849fbd24ac258.js +1 -0
- bibexpy/_web/_next/static/chunks/pages/_error-7ba65e1336b92748.js +1 -0
- bibexpy/_web/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
- bibexpy/_web/_next/static/chunks/webpack-c81f7fd28659d64f.js +1 -0
- bibexpy/_web/_next/static/css/1c4638f3e39cf127.css +3 -0
- bibexpy/_web/_next/static/ypx2IQAkZiSaFiKWnzZGB/_buildManifest.js +1 -0
- bibexpy/_web/_next/static/ypx2IQAkZiSaFiKWnzZGB/_ssgManifest.js +1 -0
- bibexpy/_web/images/PROMPTS.md +83 -0
- bibexpy/_web/images/about-illustration.png +0 -0
- bibexpy/_web/images/authors/alperen-sahin.jpg +0 -0
- bibexpy/_web/images/authors/burak-can-kara.jpg +0 -0
- bibexpy/_web/images/authors/taskin-dirsehan.jpg +0 -0
- bibexpy/_web/images/bibexpy-logo-full.png +0 -0
- bibexpy/_web/images/bibexpy-logo-header.png +0 -0
- bibexpy/_web/images/bibexpy-logo-in.png +0 -0
- bibexpy/_web/images/bibexpy-logo.webp +0 -0
- bibexpy/_web/images/hero-illustration.png +0 -0
- bibexpy/_web/images/loader.gif +0 -0
- bibexpy/_web/images/workflow-diagram.png +0 -0
- bibexpy/_web/index.html +1 -0
- bibexpy/_web/index.txt +11 -0
- bibexpy/_web/projects/_/convert/index.html +1 -0
- bibexpy/_web/projects/_/convert/index.txt +12 -0
- bibexpy/_web/projects/_/disambiguate/index.html +1 -0
- bibexpy/_web/projects/_/disambiguate/index.txt +12 -0
- bibexpy/_web/projects/_/enrich/index.html +1 -0
- bibexpy/_web/projects/_/enrich/index.txt +12 -0
- bibexpy/_web/projects/_/export/index.html +1 -0
- bibexpy/_web/projects/_/export/index.txt +12 -0
- bibexpy/_web/projects/_/merge/index.html +1 -0
- bibexpy/_web/projects/_/merge/index.txt +12 -0
- bibexpy/_web/projects/_/records/index.html +1 -0
- bibexpy/_web/projects/_/records/index.txt +12 -0
- bibexpy/_web/projects/_/report/index.html +1 -0
- bibexpy/_web/projects/_/report/index.txt +12 -0
- bibexpy/_web/projects/_/upload/index.html +1 -0
- bibexpy/_web/projects/_/upload/index.txt +12 -0
- bibexpy/_web/projects/index.html +1 -0
- bibexpy/_web/projects/index.txt +11 -0
- bibexpy/_web/settings/index.html +1 -0
- bibexpy/_web/settings/index.txt +11 -0
- bibexpy/_web/tools/README.md +65 -0
- bibexpy/_web/tools/bibexcel.svg +6 -0
- bibexpy/_web/tools/bibliometrix.png +0 -0
- bibexpy/_web/tools/biblioshiny.svg +12 -0
- bibexpy/_web/tools/citavi.svg +6 -0
- bibexpy/_web/tools/citespace.svg +7 -0
- bibexpy/_web/tools/citnetexplorer.svg +8 -0
- bibexpy/_web/tools/endnote.svg +6 -0
- bibexpy/_web/tools/excel.svg +1 -0
- bibexpy/_web/tools/gephi.svg +66 -0
- bibexpy/_web/tools/histcite.svg +7 -0
- bibexpy/_web/tools/index.html +1 -0
- bibexpy/_web/tools/index.txt +11 -0
- bibexpy/_web/tools/jabref.svg +122 -0
- bibexpy/_web/tools/latex.svg +1 -0
- bibexpy/_web/tools/mendeley.svg +1 -0
- bibexpy/_web/tools/openrefine.svg +15 -0
- bibexpy/_web/tools/overleaf.svg +1 -0
- bibexpy/_web/tools/papers.svg +7 -0
- bibexpy/_web/tools/powerbi.svg +1 -0
- bibexpy/_web/tools/python.svg +1 -0
- bibexpy/_web/tools/r.svg +1 -0
- bibexpy/_web/tools/refworks.svg +5 -0
- bibexpy/_web/tools/scite.svg +5 -0
- bibexpy/_web/tools/tableau.svg +1 -0
- bibexpy/_web/tools/vosviewer.png +0 -0
- bibexpy/_web/tools/zotero.svg +1 -0
- bibexpy/cli.py +242 -0
- bibexpy-2.0.0.dist-info/METADATA +105 -0
- bibexpy-2.0.0.dist-info/RECORD +171 -0
- bibexpy-2.0.0.dist-info/WHEEL +5 -0
- bibexpy-2.0.0.dist-info/entry_points.txt +2 -0
- bibexpy-2.0.0.dist-info/licenses/LICENSE +674 -0
- bibexpy-2.0.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
"""Rapor dışa aktarma yardımcıları.
|
|
2
|
+
|
|
3
|
+
İki tür çıktı:
|
|
4
|
+
• Ham operasyon günlüğü (audit log) → Markdown / düz metin / PDF
|
|
5
|
+
• LLM metodoloji raporu (İngilizce prose) → Markdown / düz metin / PDF
|
|
6
|
+
|
|
7
|
+
PDF üretimi reportlab ile yapılır (saf Python, sistem bağımlılığı yok).
|
|
8
|
+
Emoji ve markdown sembolleri PDF/TXT'de temizlenir; .md zaten zengin kalır.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import datetime as dt
|
|
14
|
+
import io
|
|
15
|
+
import re
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from services import audit, storage
|
|
19
|
+
|
|
20
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
21
|
+
# Metin temizleme
|
|
22
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
# Emoji + dingbat + ok işareti aralıkları (reportlab varsayılan fontu render edemez)
|
|
25
|
+
_EMOJI = re.compile(
|
|
26
|
+
"["
|
|
27
|
+
"\U0001F000-\U0001FAFF" # emoji blokları
|
|
28
|
+
"\U00002600-\U000027BF" # misc semboller + dingbatlar
|
|
29
|
+
"\U0001F1E6-\U0001F1FF" # bayraklar
|
|
30
|
+
"\U00002190-\U000021FF" # oklar (→ ↩ vb.)
|
|
31
|
+
"\U00002B00-\U00002BFF" # misc oklar/semboller
|
|
32
|
+
"️" # variation selector
|
|
33
|
+
"]+",
|
|
34
|
+
flags=re.UNICODE,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _strip_emoji(s: str) -> str:
|
|
39
|
+
return _EMOJI.sub("", str(s)).strip()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _esc_inline(s: str) -> str:
|
|
43
|
+
"""reportlab Paragraph mini-HTML için güvenli kaçış + sınırlı markdown.
|
|
44
|
+
|
|
45
|
+
Önce XML özel karakterleri kaçırılır, ardından **kalın** ve `kod` işaretleri
|
|
46
|
+
kontrollü etiketlere çevrilir.
|
|
47
|
+
"""
|
|
48
|
+
s = _strip_emoji(s)
|
|
49
|
+
s = s.replace("&", "&").replace("<", "<").replace(">", ">")
|
|
50
|
+
s = re.sub(r"\*\*(.+?)\*\*", r"<b>\1</b>", s)
|
|
51
|
+
s = re.sub(r"`(.+?)`", r'<font face="Courier">\1</font>', s)
|
|
52
|
+
return s
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
56
|
+
# PDF üretimi (markdown-benzeri metin → PDF)
|
|
57
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
def render_pdf(title: str, md_text: str) -> bytes:
|
|
60
|
+
"""Başlık + markdown-benzeri gövdeyi temiz bir PDF'e dönüştürür.
|
|
61
|
+
|
|
62
|
+
Desteklenen: # / ## / ### başlık, - / * madde, | a | b | tablo,
|
|
63
|
+
**kalın**, `kod`, boş satır = boşluk. Emoji'ler atılır.
|
|
64
|
+
"""
|
|
65
|
+
from reportlab.lib import colors
|
|
66
|
+
from reportlab.lib.enums import TA_LEFT
|
|
67
|
+
from reportlab.lib.pagesizes import A4
|
|
68
|
+
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
|
|
69
|
+
from reportlab.lib.units import cm
|
|
70
|
+
from reportlab.platypus import (
|
|
71
|
+
ListFlowable, ListItem, Paragraph, SimpleDocTemplate, Spacer, Table, TableStyle,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
buf = io.BytesIO()
|
|
75
|
+
doc = SimpleDocTemplate(
|
|
76
|
+
buf, pagesize=A4,
|
|
77
|
+
leftMargin=2 * cm, rightMargin=2 * cm, topMargin=2 * cm, bottomMargin=2 * cm,
|
|
78
|
+
title=_strip_emoji(title) or "Report", author="BibexPy v2",
|
|
79
|
+
)
|
|
80
|
+
base = getSampleStyleSheet()
|
|
81
|
+
brand = colors.HexColor("#3F32B5")
|
|
82
|
+
st_body = ParagraphStyle("body", parent=base["Normal"], fontSize=10, leading=14, alignment=TA_LEFT)
|
|
83
|
+
st_h1 = ParagraphStyle("h1", parent=base["Heading1"], fontSize=17, spaceAfter=8, textColor=brand)
|
|
84
|
+
st_h2 = ParagraphStyle("h2", parent=base["Heading2"], fontSize=13, spaceBefore=10, spaceAfter=4, textColor=colors.HexColor("#1F2A44"))
|
|
85
|
+
st_h3 = ParagraphStyle("h3", parent=base["Heading3"], fontSize=11, spaceBefore=8, spaceAfter=3)
|
|
86
|
+
st_meta = ParagraphStyle("meta", parent=base["Normal"], fontSize=8.5, textColor=colors.HexColor("#64748B"))
|
|
87
|
+
|
|
88
|
+
flow: list[Any] = []
|
|
89
|
+
flow.append(Paragraph(_esc_inline(title), st_h1))
|
|
90
|
+
flow.append(Paragraph(dt.datetime.now().strftime("%Y-%m-%d %H:%M"), st_meta))
|
|
91
|
+
flow.append(Spacer(1, 10))
|
|
92
|
+
|
|
93
|
+
lines = (md_text or "").split("\n")
|
|
94
|
+
bullets: list[str] = []
|
|
95
|
+
|
|
96
|
+
def flush_bullets() -> None:
|
|
97
|
+
nonlocal bullets
|
|
98
|
+
if bullets:
|
|
99
|
+
flow.append(ListFlowable(
|
|
100
|
+
[ListItem(Paragraph(b, st_body), leftIndent=12) for b in bullets],
|
|
101
|
+
bulletType="bullet", start="•", leftIndent=10,
|
|
102
|
+
))
|
|
103
|
+
flow.append(Spacer(1, 4))
|
|
104
|
+
bullets = []
|
|
105
|
+
|
|
106
|
+
i = 0
|
|
107
|
+
n = len(lines)
|
|
108
|
+
while i < n:
|
|
109
|
+
raw = lines[i].rstrip()
|
|
110
|
+
stripped = raw.strip()
|
|
111
|
+
|
|
112
|
+
# Tablo bloğu
|
|
113
|
+
if stripped.startswith("|"):
|
|
114
|
+
flush_bullets()
|
|
115
|
+
rows: list[list[str]] = []
|
|
116
|
+
while i < n and lines[i].strip().startswith("|"):
|
|
117
|
+
cells = [c.strip() for c in lines[i].strip().strip("|").split("|")]
|
|
118
|
+
i += 1
|
|
119
|
+
# ayraç satırı ( |---|---| ) atla
|
|
120
|
+
if cells and all(set(c) <= set("-: ") for c in cells):
|
|
121
|
+
continue
|
|
122
|
+
rows.append(cells)
|
|
123
|
+
if rows:
|
|
124
|
+
ncol = max(len(r) for r in rows)
|
|
125
|
+
data = [[Paragraph(_esc_inline(c), st_body) for c in (r + [""] * (ncol - len(r)))] for r in rows]
|
|
126
|
+
tbl = Table(data, hAlign="LEFT")
|
|
127
|
+
tbl.setStyle(TableStyle([
|
|
128
|
+
("BACKGROUND", (0, 0), (-1, 0), brand),
|
|
129
|
+
("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
|
|
130
|
+
("FONTSIZE", (0, 0), (-1, -1), 9),
|
|
131
|
+
("GRID", (0, 0), (-1, -1), 0.4, colors.HexColor("#CBD5E1")),
|
|
132
|
+
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
|
|
133
|
+
("LEFTPADDING", (0, 0), (-1, -1), 5),
|
|
134
|
+
("RIGHTPADDING", (0, 0), (-1, -1), 5),
|
|
135
|
+
("TOPPADDING", (0, 0), (-1, -1), 3),
|
|
136
|
+
("BOTTOMPADDING", (0, 0), (-1, -1), 3),
|
|
137
|
+
]))
|
|
138
|
+
flow.append(tbl)
|
|
139
|
+
flow.append(Spacer(1, 6))
|
|
140
|
+
continue
|
|
141
|
+
|
|
142
|
+
if not stripped:
|
|
143
|
+
flush_bullets()
|
|
144
|
+
flow.append(Spacer(1, 5))
|
|
145
|
+
i += 1
|
|
146
|
+
continue
|
|
147
|
+
if stripped.startswith("### "):
|
|
148
|
+
flush_bullets(); flow.append(Paragraph(_esc_inline(stripped[4:]), st_h3)); i += 1; continue
|
|
149
|
+
if stripped.startswith("## "):
|
|
150
|
+
flush_bullets(); flow.append(Paragraph(_esc_inline(stripped[3:]), st_h2)); i += 1; continue
|
|
151
|
+
if stripped.startswith("# "):
|
|
152
|
+
flush_bullets(); flow.append(Paragraph(_esc_inline(stripped[2:]), st_h1)); i += 1; continue
|
|
153
|
+
if stripped.startswith(("- ", "* ")):
|
|
154
|
+
bullets.append(_esc_inline(stripped[2:])); i += 1; continue
|
|
155
|
+
|
|
156
|
+
flush_bullets()
|
|
157
|
+
flow.append(Paragraph(_esc_inline(stripped), st_body))
|
|
158
|
+
i += 1
|
|
159
|
+
|
|
160
|
+
flush_bullets()
|
|
161
|
+
doc.build(flow)
|
|
162
|
+
return buf.getvalue()
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
166
|
+
# Ham günlük → düz metin
|
|
167
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
def format_text_report(project_id: str) -> str:
|
|
170
|
+
"""Operasyon günlüğünün temiz, markdown'sız, emojisiz düz metin sürümü."""
|
|
171
|
+
meta = storage.get_project(project_id)
|
|
172
|
+
entries = audit.read(project_id)
|
|
173
|
+
name = meta.name if meta else project_id
|
|
174
|
+
|
|
175
|
+
out: list[str] = []
|
|
176
|
+
out.append("BibexPy v2 - Operation Report")
|
|
177
|
+
out.append("=" * 40)
|
|
178
|
+
out.append(f"Project : {name} ({project_id})")
|
|
179
|
+
out.append(f"Generated : {dt.datetime.now().strftime('%Y-%m-%d %H:%M')}")
|
|
180
|
+
out.append(f"Total operations: {len(entries)}")
|
|
181
|
+
out.append("")
|
|
182
|
+
|
|
183
|
+
if not entries:
|
|
184
|
+
out.append("No operations recorded yet.")
|
|
185
|
+
return "\n".join(out)
|
|
186
|
+
|
|
187
|
+
s = audit.summary(project_id)
|
|
188
|
+
out.append("Summary")
|
|
189
|
+
out.append("-" * 40)
|
|
190
|
+
for k, c in sorted(s.get("by_kind", {}).items(), key=lambda x: -x[1]):
|
|
191
|
+
label = _strip_emoji(audit.KIND_LABELS.get(k, k))
|
|
192
|
+
out.append(f" {label:<28} {c}")
|
|
193
|
+
out.append("")
|
|
194
|
+
|
|
195
|
+
out.append("Chronology")
|
|
196
|
+
out.append("-" * 40)
|
|
197
|
+
for idx, e in enumerate(entries, 1):
|
|
198
|
+
ts = e.get("ts")
|
|
199
|
+
ts_str = dt.datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M:%S") if ts else "-"
|
|
200
|
+
label = _strip_emoji(audit.KIND_LABELS.get(e.get("kind", ""), e.get("kind", "?")))
|
|
201
|
+
title = _strip_emoji(e.get("title", ""))
|
|
202
|
+
out.append(f"{idx}. [{label}] {title}")
|
|
203
|
+
out.append(f" {ts_str}")
|
|
204
|
+
details = e.get("details") or {}
|
|
205
|
+
for k, v in details.items():
|
|
206
|
+
if v is None or v == "":
|
|
207
|
+
continue
|
|
208
|
+
if isinstance(v, (list, dict)):
|
|
209
|
+
v = str(v)
|
|
210
|
+
v = str(v)
|
|
211
|
+
if len(v) > 160:
|
|
212
|
+
v = v[:160] + "..."
|
|
213
|
+
out.append(f" - {k}: {v}")
|
|
214
|
+
if e.get("snapshot"):
|
|
215
|
+
out.append(f" - snapshot: {str(e['snapshot']).split('/')[-1].split(chr(92))[-1]}")
|
|
216
|
+
out.append("")
|
|
217
|
+
return "\n".join(out)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
221
|
+
# LLM metodoloji raporu için günlük digest'i
|
|
222
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
# Günlük kind → İngilizce aksiyon etiketi (LLM'in İngilizce yazması için)
|
|
225
|
+
_ACTION_EN = {
|
|
226
|
+
"project_create": "Project created",
|
|
227
|
+
"upload": "Source files uploaded",
|
|
228
|
+
"convert": "Raw files converted to tabular format",
|
|
229
|
+
"merge": "WoS/Scopus records merged and deduplicated",
|
|
230
|
+
"merge_borderline": "Borderline duplicate pairs reviewed manually",
|
|
231
|
+
"analysis_activate": "Active analysis selected",
|
|
232
|
+
"analysis_delete": "Analysis removed",
|
|
233
|
+
"filter_save": "Filter/screening preset defined",
|
|
234
|
+
"records_delete": "Records removed (screening/exclusion)",
|
|
235
|
+
"enrich_api": "Missing fields enriched via external APIs (e.g. Crossref/OpenAlex)",
|
|
236
|
+
"enrich_ml": "Missing fields enriched via machine-learning prediction",
|
|
237
|
+
"enrich_field": "Specific field enriched",
|
|
238
|
+
"enrich_selected": "Selected records enriched",
|
|
239
|
+
"enrich_selected_requested": "Selected records enriched",
|
|
240
|
+
"disambiguate": "Author/affiliation names disambiguated (normalization/splitting)",
|
|
241
|
+
"disambiguate_authors": "Author names disambiguated",
|
|
242
|
+
"disambiguate_affiliations": "Affiliation names standardized",
|
|
243
|
+
"snapshot_restore": "Dataset restored from a previous snapshot",
|
|
244
|
+
"export": "Prepared dataset exported",
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _clean_metrics(details: dict) -> dict:
|
|
249
|
+
"""Digest için gürültüsüz metrik alt kümesi — sayılar + kısa değerler + küçük listeler."""
|
|
250
|
+
out: dict[str, Any] = {}
|
|
251
|
+
for k, v in (details or {}).items():
|
|
252
|
+
if v is None or v == "":
|
|
253
|
+
continue
|
|
254
|
+
if isinstance(v, bool) or isinstance(v, (int, float)):
|
|
255
|
+
out[k] = v
|
|
256
|
+
elif isinstance(v, str):
|
|
257
|
+
if len(v) <= 120:
|
|
258
|
+
out[k] = v
|
|
259
|
+
elif isinstance(v, list):
|
|
260
|
+
if len(v) <= 12 and all(isinstance(x, (str, int, float)) for x in v):
|
|
261
|
+
short = [x for x in v if not (isinstance(x, str) and len(x) > 80)]
|
|
262
|
+
if short:
|
|
263
|
+
out[k] = short
|
|
264
|
+
elif isinstance(v, dict):
|
|
265
|
+
if len(v) <= 12:
|
|
266
|
+
out[k] = v
|
|
267
|
+
return out
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def build_log_digest(project_id: str) -> dict:
|
|
271
|
+
"""LLM'e gönderilecek temiz, İngilizce-aksiyonlu kronolojik digest."""
|
|
272
|
+
meta = storage.get_project(project_id)
|
|
273
|
+
entries = audit.read(project_id)
|
|
274
|
+
steps: list[dict] = []
|
|
275
|
+
for e in entries:
|
|
276
|
+
kind = e.get("kind", "")
|
|
277
|
+
# job runner her job için completed/failed kaydı yazar; sadece anlamlı olanları al
|
|
278
|
+
ua = e.get("user_action") or ""
|
|
279
|
+
if ua.startswith("job_") and ua != "job_completed":
|
|
280
|
+
continue # failed/cancelled/queued/running ara kayıtları atla
|
|
281
|
+
ts = e.get("ts")
|
|
282
|
+
steps.append({
|
|
283
|
+
"ts": dt.datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M") if ts else None,
|
|
284
|
+
"kind": kind,
|
|
285
|
+
"action": _ACTION_EN.get(kind, kind.replace("_", " ")),
|
|
286
|
+
"metrics": _clean_metrics(e.get("details") or {}),
|
|
287
|
+
})
|
|
288
|
+
# Toplam kayıt sayısı (varsa) — bağlam için
|
|
289
|
+
total_records = None
|
|
290
|
+
try:
|
|
291
|
+
from services import filter_engine
|
|
292
|
+
df = filter_engine.load_merged(project_id)
|
|
293
|
+
total_records = int(len(df))
|
|
294
|
+
except Exception:
|
|
295
|
+
total_records = None
|
|
296
|
+
return {
|
|
297
|
+
"project_name": meta.name if meta else project_id,
|
|
298
|
+
"current_record_count": total_records,
|
|
299
|
+
"steps": steps,
|
|
300
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""DataFrame -> RIS yazıcı."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import pandas as pd
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
TYPE_MAP = {
|
|
12
|
+
"ARTICLE": "JOUR",
|
|
13
|
+
"REVIEW": "JOUR",
|
|
14
|
+
"CONFERENCE PAPER": "CPAPER",
|
|
15
|
+
"BOOK": "BOOK",
|
|
16
|
+
"BOOK CHAPTER": "CHAP",
|
|
17
|
+
"EDITORIAL": "JOUR",
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _ty(dt: str) -> str:
|
|
22
|
+
return TYPE_MAP.get(str(dt).upper().strip(), "JOUR")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _split(value: str) -> list[str]:
|
|
26
|
+
return [v.strip() for v in re.split(r";", str(value)) if v.strip()]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def write_ris(df: pd.DataFrame, output: Path) -> Path:
|
|
30
|
+
with output.open("w", encoding="utf-8") as f:
|
|
31
|
+
for _, row in df.iterrows():
|
|
32
|
+
f.write(f"TY - {_ty(row.get('DT', '') or '')}\n")
|
|
33
|
+
for au in _split(row.get("AU", "")):
|
|
34
|
+
f.write(f"AU - {au}\n")
|
|
35
|
+
ti = str(row.get("TI", "") or "").strip()
|
|
36
|
+
if ti: f.write(f"TI - {ti}\n")
|
|
37
|
+
so = str(row.get("SO", "") or "").strip()
|
|
38
|
+
if so: f.write(f"JO - {so}\n")
|
|
39
|
+
ji = str(row.get("JI", "") or "").strip()
|
|
40
|
+
if ji: f.write(f"J2 - {ji}\n")
|
|
41
|
+
py = str(row.get("PY", "") or "").strip()
|
|
42
|
+
if py: f.write(f"PY - {py}\n")
|
|
43
|
+
vl = str(row.get("VL", "") or "").strip()
|
|
44
|
+
if vl: f.write(f"VL - {vl}\n")
|
|
45
|
+
issue = str(row.get("IS", "") or "").strip()
|
|
46
|
+
if issue: f.write(f"IS - {issue}\n")
|
|
47
|
+
pg = str(row.get("PG", "") or "").strip()
|
|
48
|
+
if pg: f.write(f"SP - {pg}\n")
|
|
49
|
+
di = str(row.get("DI", "") or "").strip()
|
|
50
|
+
if di: f.write(f"DO - {di}\n")
|
|
51
|
+
url = str(row.get("URL", "") or "").strip()
|
|
52
|
+
if url: f.write(f"UR - {url}\n")
|
|
53
|
+
ab = str(row.get("AB", "") or "").strip()
|
|
54
|
+
if ab: f.write(f"AB - {ab}\n")
|
|
55
|
+
for kw in _split(row.get("DE", "")):
|
|
56
|
+
f.write(f"KW - {kw}\n")
|
|
57
|
+
pu = str(row.get("PU", "") or "").strip()
|
|
58
|
+
if pu: f.write(f"PB - {pu}\n")
|
|
59
|
+
sn = str(row.get("SN", "") or "").strip()
|
|
60
|
+
if sn: f.write(f"SN - {sn}\n")
|
|
61
|
+
f.write("ER - \n\n")
|
|
62
|
+
return output
|