File size: 47,281 Bytes
406a5e6 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 | """Navigational ("specific") PFB solver: resolve a colloquial paper mention.
Compact faithful port of the IRIS v2 specific chain
(``iris_asta/iris_asta/solvers/pfb.py``: resolution chain ~3846-3900,
``_grounded_reference_resolve`` ~649, ``_relevance_resolve`` ~1142,
``_hedge_specific`` ~3713) onto the pfbmax contract interfaces.
Chain: (a) one LLM call -> {canonical_title, first_author, year,
all_plausible_titles} (caret-stripped: the corpus title index collapses
'^' out of titles, so a literal caret never matches); (b) exact title search on
canonical + plausibles; (c) guarded relevance-resolve (exact normalized-title
match, else highest-citationCount hit passing a >=60% title-word-overlap
anchor); (d) grounded reference walk (references of the nickname's own top
hits, ranked by co-citation votes, one LLM index-pick). Every candidate from
(b)/(c) must VERIFY against the LLM's own metadata: title overlap with the
searched string OR (author-surname match AND year +/-1); a mismatch falls
through to the next link.
Cardinality (exact-set F1: every wrong extra craters precision):
* default: emit exactly 1;
* hedge to 3 only when signals conflict: the query's own year/author cues
reject the resolution, or the mention is a bare generic acronym ("the cnn
paper" names a class, not a paper);
* bare named-artifact mentions ("the <name> paper"): when >=2 verified
DISTINCT corpus papers carry the artifact as their leading title token
(disjoint author sets, non-version, dissimilar titles), the name is
contested; emit up to 5 by citationCount.
Integrity: logic keys on query text + general rules
only; no gold ids, no per-query branching; evidence is verbatim corpus text
("«Title» (year): abstract[:300]"). Stdlib only.
"""
from __future__ import annotations
import os
import re
from typing import Any
Submission = list[tuple[str, str]]
# --------------------------------------------------------------------------
# Tunables (general rules - never per-query)
# --------------------------------------------------------------------------
_MAX_TITLE_PROBES = 4 # canonical + up to 3 plausibles
_RELEVANCE_LIMIT = 10 # relevance-resolve fallback depth
_TITLE_OVERLAP = 0.6 # word-overlap anchor (v2 _ANCHOR_WORD_OVERLAP)
_MIN_OVERLAP_WORDS = 2 # overlap verification needs >=2 content words
# (a bare acronym overlapping itself is circular)
_REFWALK_SEEDS = 3 # grounded walk: top hits whose refs vote
_REFWALK_REF_LIMIT = 1000 # references pulled per seed (slim fields)
_REFWALK_TITLE_CAP = 50 # candidate titles shown to the LLM
_HEDGE_TOTAL = 3 # resolved + 2 extras when signals conflict
_MULTI_MAX = 5 # cap for contested-artifact multi-emit
_MULTI_MIN_DISTINCT = 2 # non-resolved distinct hits needed to fire
_PROBE_LIMIT = 30 # relevance hits scanned by the artifact probe
_PROBE_MIN_CITES = 10 # noise floor for probe candidates
_ANCHOR_MIN_CITES = 50 # prominence floor for artifact-anchor resolution
_DISTINCT_TITLE_OVERLAP = 0.5 # >= this residual overlap => same work
_AUTHOR_PREFIX = 4 # author cue match on shared 4-char prefix
_EVIDENCE_ABSTRACT_CHARS = 300
_HYDRATE_CAP = 5 # get_paper calls to fill missing abstracts
_STOPWORDS = frozenset(
"the a an and or of in on for with to by at as is are was were from "
"that this its not about paper papers".split()
)
_FILLER = _STOPWORDS | frozenset(
"find locate get show me please original famous classic seminal known "
"et al work works article publication".split()
)
#: Common research nouns that can never BE a named artifact ("the paper
#: about the ACME dataset": 'dataset' describes, 'ACME' names).
_COMMON_NOUNS = frozenset(
"dataset datasets model models benchmark benchmarks corpus corpora "
"system systems method methods approach approaches framework frameworks "
"database databases survey surveys task tasks challenge challenges "
"network networks learning search analysis data algorithm algorithms "
"architecture architectures technique techniques study studies".split()
)
_VERSION_TOKENS = frozenset(
"xl xxl xs small base large mini nano tiny huge plus pro turbo lite max "
"v2 v3 v4 ii iii iv 2 3 4".split()
)
_LEADING_ARTICLES = ("the", "a", "an")
_YEAR_CUE_RE = re.compile(r"(?<![0-9])((?:19|20)[0-9]{2})(?![0-9])")
_GLUED_AUTHOR_RE = re.compile(r"([A-Za-z]{3,})(?=(?:19|20)[0-9]{2})")
_BY_AUTHOR_RE = re.compile(r"\bby\s+([A-Z][A-Za-z'\-]{1,})")
_META_SYSTEM = """You identify the ONE scientific paper a colloquial mention refers to.
The user names a paper by nickname, acronym, dataset/system/model name, or
author+year (e.g. "the gpt-2 paper", "the SQuAD paper", "BART by Lewis et
al."). Return the paper's canonical title EXACTLY as published, in PLAIN
ASCII — never use ^, superscripts, subscripts, or special glyphs (write
"MS2" not "MS^2"). Do NOT expand an acronym into a descriptive phrase if the
real published title is different — return the real title (e.g. "the gpt-2
paper" -> "Language Models are Unsupervised Multitask Learners"). Also
return the first author's SURNAME and the publication year.
If the mention could plausibly refer to SEVERAL well-known DISTINCT papers
(a generic class like "the cnn paper", or a name shared by unrelated
systems), list the strongest 2-3 OTHER candidates' exact titles in
"all_plausible_titles" (do not repeat canonical_title; never list mere
follow-ups, extensions, or versions of the same work). Otherwise use [].
If you do not confidently know the exact title, use null.
Respond with JSON only:
{"canonical_title": str|null, "first_author": str|null, "year": int|null,
"all_plausible_titles": [str, ...]}"""
_SELECT_SYSTEM = """You identify which paper a colloquial reference points to, by GROUNDED selection.
You are given a query that names ONE scientific paper by nickname, acronym,
dataset/system name, or author+year, plus a NUMBERED list of candidate paper
titles (the references of papers retrieved for that nickname — papers that
use a system cite its canonical paper). Pick the ONE candidate whose title is
the canonical published paper the query refers to. Do NOT guess from outside
knowledge: if no listed candidate matches, return null.
Respond with JSON only: {"index": <1-based candidate number>|null}"""
# --------------------------------------------------------------------------
# Small deterministic helpers
# --------------------------------------------------------------------------
def _norm(s: str) -> str:
"""Lowercase, strip punctuation to spaces (fuzzy-but-deterministic)."""
return re.sub(r"\s+", " ", re.sub(r"[^a-z0-9 ]+", " ", (s or "").lower())).strip()
def _cid_of(paper: Any) -> str | None:
"""Digits-only corpus id of a paper-like object, or None."""
raw = getattr(paper, "corpus_id", None)
if raw is None:
return None
s = str(raw).strip().lower().removeprefix("corpusid:").strip()
return str(int(s)) if s.isdigit() else None
def _cites(paper: Any) -> int:
extra = getattr(paper, "extra", None)
value = extra.get("citationCount") if isinstance(extra, dict) else None
if value is None:
value = getattr(paper, "citationCount", None)
try:
return int(value)
except (TypeError, ValueError):
return 0
def _content_words(s: str) -> set[str]:
return {w for w in _norm(s).split() if len(w) >= 3 and w not in _STOPWORDS}
def _overlap(searched: str, title: str) -> float:
"""Fraction of the searched string's content words found in ``title``."""
want = _content_words(searched)
if not want:
return 0.0
have = set(_norm(title).split())
return sum(1 for w in want if w in have) / len(want)
def _lev1(a: str, b: str) -> bool:
"""Edit distance <= 1 ('smth' passes for 'smith')."""
if a == b:
return True
if abs(len(a) - len(b)) > 1:
return False
if len(a) > len(b):
a, b = b, a
i = 0
while i < len(a) and a[i] == b[i]:
i += 1
if len(a) == len(b):
return a[i + 1:] == b[i + 1:]
return a[i:] == b[i + 1:]
def _author_tokens(paper: Any) -> list[str]:
tokens: list[str] = []
for record in getattr(paper, "authors", None) or []:
if isinstance(record, dict):
name = record.get("name")
else:
name = getattr(record, "name", record)
tokens.extend(_norm(str(name or "")).split())
return tokens
def _author_match(paper: Any, cues: list[str]) -> bool:
"""Any author-name token verifies a cue: shared 4-char prefix,
containment either way, or edit distance <= 1; short tokens (<4) only on
exact equality ('he' for Kaiming He)."""
for token in _author_tokens(paper):
for cue in cues:
if not cue:
continue
if len(token) < _AUTHOR_PREFIX or len(cue) < _AUTHOR_PREFIX:
if token == cue:
return True
continue
if (token[:_AUTHOR_PREFIX] == cue[:_AUTHOR_PREFIX]
or cue in token or token in cue or _lev1(cue, token)):
return True
return False
def _violates_cutoff(paper: Any, inserted_before: str | None) -> bool:
"""Year strictly after the snapshot year PROVES the id is invalid."""
if not inserted_before:
return False
try:
cutoff_year = int(str(inserted_before)[:4])
except (TypeError, ValueError):
return False
year = getattr(paper, "year", None)
return isinstance(year, int) and not isinstance(year, bool) and year > cutoff_year
def _paper_evidence(paper: Any) -> str:
title = getattr(paper, "title", "") or "(untitled)"
year = getattr(paper, "year", None)
year_str = str(year) if isinstance(year, int) and not isinstance(year, bool) else "n.d."
abstract = (getattr(paper, "abstract", "") or "")[:_EVIDENCE_ABSTRACT_CHARS]
return f"«{title}» ({year_str}): {abstract}".strip()
# --------------------------------------------------------------------------
# Query-shape extraction (deterministic; no LLM judgment)
# --------------------------------------------------------------------------
def _raw_content_tokens(query: str) -> list[str]:
"""Whitespace tokens minus filler, caret-stripped, edge-punct trimmed."""
out = []
for tok in (query or "").split():
tok = tok.strip(".,;:!?()[]{}\"'").replace("^", "")
if not tok:
continue
if _norm(tok).replace(" ", "") in _FILLER:
continue
out.append(tok)
return out
def _generic_acronym(query: str) -> bool:
"""Every content token is a bare <=3-char alpha acronym ('the cnn
paper' names an architecture class, not one paper)."""
tokens = [_norm(t).replace(" ", "") for t in _raw_content_tokens(query)]
return bool(tokens) and all(t.isalpha() and len(t) <= 3 for t in tokens)
def _bare_artifact(query: str) -> str | None:
"""The single named-artifact token of a bare mention ('the ACME paper'
-> 'ACME'), or None when the mention carries qualifiers/cues. Only
plain alphabetic names are contestable: a digit-bearing name ("gpt-2")
is already version-disambiguated and denotes ONE artifact."""
tokens = _raw_content_tokens(query)
if len(tokens) != 1:
return None
tok = tokens[0]
if len(tok) < 4 or _generic_acronym(query):
return None
if not _norm(tok).replace(" ", "").isalpha() or " " in _norm(tok):
return None
return tok
def _query_cues(query: str) -> tuple[set[int], list[str]]:
"""(year cues, author-name cues): 4-digit years anywhere, author names
glued to a year ('Smith2021'), and 'by <Name>' phrasing."""
years = {int(y) for y in _YEAR_CUE_RE.findall(query or "")}
authors = [_norm(a) for a in _GLUED_AUTHOR_RE.findall(query or "")]
for m in _BY_AUTHOR_RE.findall(query or ""):
a = _norm(m)
if a and a not in ("the", "a", "an") and a not in authors:
authors.append(a)
authors = [a for a in authors if len(a) >= 2]
return years, authors
def _fails_query_cues(query: str, paper: Any | None) -> bool:
"""v2 hedge core: True when the query's own year/author cues reject the
resolution (extraction + comparison; no LLM judgment)."""
years, cues = _query_cues(query)
if paper is None:
return bool(years or cues)
if years:
year = getattr(paper, "year", None)
if not isinstance(year, int) or isinstance(year, bool) or \
all(abs(year - y) > 1 for y in years):
return True
if cues and not _author_match(paper, cues):
return True
return False
# --------------------------------------------------------------------------
# LLM metadata + verification
# --------------------------------------------------------------------------
def _clean_title(value: Any) -> str | None:
if not isinstance(value, str):
return None
t = value.replace("^", "").strip()
if not t or t.lower() in ("null", "none", "unknown"):
return None
return t
def _mention_meta(query: str, llm: Any) -> dict:
"""One LLM call: canonical title + first-author surname + year +
plausible distinct alternates. Soft-fails to an empty meta."""
try:
obj = llm.json(_META_SYSTEM, query, max_tokens=300) or {}
except Exception:
obj = {}
canonical = _clean_title(obj.get("canonical_title"))
surname = obj.get("first_author")
surname = _norm(surname).split()[-1] if isinstance(surname, str) and _norm(surname) else None
year = obj.get("year")
if isinstance(year, bool) or not isinstance(year, int) or not 1000 <= year <= 2100:
year = None
plausibles = []
for t in obj.get("all_plausible_titles") or []:
ct = _clean_title(t)
if ct and _norm(ct) != _norm(canonical or "") and \
_norm(ct) not in {_norm(p) for p in plausibles}:
plausibles.append(ct)
return {"canonical": canonical, "surname": surname, "year": year,
"plausibles": plausibles[:_MAX_TITLE_PROBES - 1]}
def _mention_tokens(query: str) -> list[str]:
"""The mention's own distinctive artifact-name candidates, most
distinctive first (digit-bearing or mixed-case beat capitalized beat
lowercase; longer beats shorter), normalized and space-collapsed.
These anchor verification to what the USER wrote. The LLM's canonical
title cannot be trusted for that job: it hallucinates confidently
(measured: an invented title matched an unrelated paper at 0.71 word
overlap and passed verification, because the overlap was computed
against the hallucination rather than the mention).
"""
scored = []
for tok in _raw_content_tokens(query):
n = _norm(tok).replace(" ", "")
if not n or len(n) < 2 or n in _COMMON_NOUNS:
continue
has_digit = any(c.isdigit() for c in tok)
mixed = any(c.isupper() for c in tok[1:])
rank = 2 if (has_digit or mixed) else (1 if tok[:1].isupper() else 0)
scored.append(((rank, len(n)), n))
scored.sort(reverse=True)
out, seen = [], set()
for _s, n in scored:
if n not in seen:
seen.add(n)
out.append(n)
return out
def _title_links_mention(title: str, tokens: list[str]) -> bool:
"""The candidate title actually contains one of the mention's distinctive
tokens (space-collapsed, so a caret-bearing mention like 'X^2' becomes
'x2' and matches a title beginning 'X2:')."""
if not tokens:
return True # nothing distinctive to anchor on
collapsed = _norm(title).replace(" ", "")
return any(t in collapsed for t in tokens)
def _is_version_successor(title: str, artifact_norm: str) -> bool:
"""Title names artifact+version ('Acme2', 'Acme-XL'), which
is a successor work, not the original the mention asks for."""
art = artifact_norm.replace(" ", "")
if not art:
return False
tn = _norm(title)
collapsed = tn.replace(" ", "")
idx = collapsed.find(art)
if idx >= 0:
rest = collapsed[idx + len(art):]
if rest[:1].isdigit():
return True
parts = tn.split()
for i, p in enumerate(parts):
if p == art and i + 1 < len(parts) and parts[i + 1] in _VERSION_TOKENS:
return True
return False
def _verify_meta(paper: Any, searched: str, meta: dict,
mention_tokens: list[str] | None = None) -> bool:
"""Accept a candidate only when it matches what the LLM said it was
looking for: >=60% title-word overlap with the searched string (needs
>=2 content words, since a bare acronym self-overlapping is circular), OR
first-author surname match AND year within +/-1 (missing years pass
vacuously; a PRESENT year >1 off rejects).
The fuzzy-overlap branch additionally requires the title to LINK to the
mention itself, closing the circular-verification hole above. Exact
normalized-title equality and the author+year branch are exempt (they are
already anchored on something the model did not invent)."""
title = getattr(paper, "title", "") or ""
want = _content_words(searched)
if _norm(title) and _norm(title) == _norm(searched):
return True
if len(want) >= _MIN_OVERLAP_WORDS and _overlap(searched, title) >= _TITLE_OVERLAP:
if _title_links_mention(title, mention_tokens or []):
return True
surname = meta.get("surname")
if surname and _author_match(paper, [surname]):
want_year = meta.get("year")
year = getattr(paper, "year", None)
if want_year is None or not isinstance(year, int) or isinstance(year, bool):
return True
if abs(year - want_year) <= 1:
return True
return False
# --------------------------------------------------------------------------
# Resolution links (each soft-fails to None so the next link runs)
# --------------------------------------------------------------------------
def _title_search(client: Any, title: str) -> Any | None:
try:
return client.search_paper_by_title(title)
except Exception:
return None
def _paper_search(client: Any, text: str, limit: int,
inserted_before: str | None) -> list:
try:
try:
hits = client.paper_search(
text, limit=limit, publication_date_before=inserted_before)
except TypeError: # duck-typed client without the date kwarg
hits = client.paper_search(text, limit=limit)
except Exception:
return []
return [h for h in hits or [] if _cid_of(h)]
def _relevance_resolve(client: Any, searched: str,
inserted_before: str | None) -> Any | None:
"""Word-overlap-anchored relevance resolution: exact normalized-title
match wins; else the highest-citationCount hit sharing >=60% of the
searched string's content words (the anchor keeps same-keyword noise out;
without it, a bare artifact name would match topically unrelated papers
from another field that happen to share the word)."""
hits = _paper_search(client, searched, _RELEVANCE_LIMIT, inserted_before)
if not hits:
return None
want_norm = _norm(searched)
for hit in hits:
if _norm(getattr(hit, "title", "") or "") == want_norm:
return hit
best = None
for hit in hits:
if _overlap(searched, getattr(hit, "title", "") or "") >= _TITLE_OVERLAP \
and (best is None or _cites(hit) > _cites(best)):
best = hit
return best
_FOCUSED_SYSTEM = (
"Give the EXACT published title of the paper this mention refers to. "
'Output ONLY JSON: {"title": "<exact title>"}. If you are not confident '
'of the exact title, output {"title": null} rather than guessing.'
)
def _focused_canonical(query: str, llm: Any, already: str | None) -> str | None:
"""Second chance at the canonical title with IRIS v2's narrow prompt
shape (one question, one answer). The combined metadata call has to
produce four fields at once and drifts; asking only for the title
recovers some of those cases."""
try:
obj = llm.json(_FOCUSED_SYSTEM, query, max_tokens=120) or {}
except Exception:
return None
title = _clean_title(obj.get("title"))
if title and _norm(title) != _norm(already or ""):
return title
return None
_TOPIC_SYSTEM = (
"You know the scientific literature. Given a mention of an artifact "
"(dataset, benchmark, model, system or algorithm), reply JSON only: "
'{"topic": "..."} -- EXACTLY 3 to 5 lowercase content words naming the '
"TASK and DOMAIN the artifact is for (e.g. 'summarization medical "
"studies'). Never invent a paper title. If you do not recognise the "
'artifact, reply {"topic": ""}.'
)
def _topic_direct(query: str, model: str):
"""One direct chat call, used when the injected LLM cannot pick a model.
Reads the API key from the environment and falls back to a local key
file if present."""
import json as _json
import urllib.request as _u
key = ""
env = os.environ.get("OPENAI_API_KEY", "")
if env:
key = env
else:
here = os.path.dirname(os.path.abspath(__file__))
try:
key = open(os.path.join(here, ".openai_key"), encoding="utf-8").read().strip()
except Exception:
return None
body = _json.dumps({
"model": model,
"messages": [{"role": "system", "content": _TOPIC_SYSTEM},
{"role": "user", "content": query}],
"temperature": 0, "max_tokens": 60,
"response_format": {"type": "json_object"}}).encode()
req = _u.Request("https://api.openai.com/v1/chat/completions", data=body,
headers={"Content-Type": "application/json",
"Authorization": "Bearer " + key})
try:
with _u.urlopen(req, timeout=60) as r:
out = _json.loads(r.read())
return _json.loads((out["choices"][0]["message"] or {}).get("content") or "{}")
except Exception:
return None
def _artifact_topic_resolve(query: str, meta: dict, client: Any, llm: Any,
inserted_before: str | None):
"""Resolve via the artifact token plus a SHORT topic phrase.
Motivating failure: for a lesser-known artifact the model invents a
canonical title, so every title-driven link chases a paper that does not
exist, and the bare artifact token alone lands on an unrelated paper from
another field that happens to share the token.
The split that makes this work: models are unreliable at reproducing exact
TITLES but reliable about what an artifact IS FOR. Asked for a title, even
a strong model invents a plausible-sounding one; asked what the artifact
is about, it answers with the correct task and domain. So we ask only for
the topic and let the corpus supply the title.
Probe length matters: a short "<artifact> <task> <domain>" probe resolves
the right paper, while the same terms padded out to nine words resolve
nothing -- hence the hard 3-5 word cap in the prompt and the trim below.
Every candidate still goes through ``_verify_meta``, so this widens what we
look for without widening what we accept.
"""
tokens = _mention_tokens(query)
if not tokens:
return None, None
# This call needs KNOWLEDGE, not reasoning: the small backbone model does
# not recognise lesser-known artifacts (it answers with generic words like
# "dataset, machine learning, benchmark") while the larger model does.
# Falling back to the cheap backbone therefore silently disables this
# link, which is exactly what happened in one evaluation run where the
# harness supplied an LLM wrapper whose .json() rejects a `model` kwarg.
# So go direct when the wrapper cannot honour the override.
want = os.environ.get("PFBMAX_TOPIC_MODEL", "gpt-4o-2024-11-20")
obj = None
try:
obj = llm.json(_TOPIC_SYSTEM, query, max_tokens=60, model=want)
except TypeError:
obj = _topic_direct(query, want)
except Exception:
return None, None
if not obj:
return None, None
topic = " ".join(str(obj.get("topic") or "").replace(",", " ").split())
if not topic:
return None, None
topic = " ".join(topic.split()[:5])
for token in tokens[:_MAX_TITLE_PROBES]:
probe = f"{token} {topic}"
paper = _relevance_resolve(client, probe, inserted_before)
if paper is None or _cid_of(paper) is None:
continue
if _violates_cutoff(paper, inserted_before):
continue
if _verify_meta(paper, probe, meta, tokens):
return paper, probe
return None, None
def _artifact_author_resolve(query: str, meta: dict, client: Any,
inserted_before: str | None):
"""Search the mention's OWN artifact token together with the author cue.
Motivating failure: for a lesser-known artifact the LLM invents a
plausible canonical title, so every title-driven link searches for
something that does not exist, and the artifact-token link, which only
knows the bare token, lands on an unrelated paper from another field that
happens to share the token. The query itself carries the disambiguator: an
author-year tag. `_verify_meta` can already check it (surnames match on a
shared 4-char prefix, so a slightly misspelled surname in the query still
verifies the real author), but nothing ever RETRIEVES that paper.
So probe the artifact token AND the surname together. This link only runs
when the mention actually supplies an author cue, and every candidate is
still verified, so it cannot loosen anything.
"""
surname = (meta or {}).get("surname")
if not surname:
return None, None
tokens = _mention_tokens(query)
if not tokens:
return None, None
for token in tokens[:_MAX_TITLE_PROBES]:
for probe in (f"{token} {surname}", f"{surname} {token}"):
paper = _relevance_resolve(client, probe, inserted_before)
if paper is None or _cid_of(paper) is None:
continue
if _violates_cutoff(paper, inserted_before):
continue
# verified on the author+year branch, which is anchored on the
# query text rather than on anything the model generated
if _verify_meta(paper, token, meta, tokens):
return paper, probe
return None, None
def _artifact_anchor_resolve(query: str, client: Any,
inserted_before: str | None
) -> tuple[Any | None, str | None]:
"""Resolve by searching the mention's OWN distinctive token, never a
model-generated title. Three tiers, each version-successor filtered:
1. title LEADS with the artifact ('<Artifact>: ...' system-paper style)
2. title CONTAINS the artifact
3. abstract mentions the artifact (some systems are introduced by a
paper whose title never names them)
A tier resolves when the query's own year/author cues single a survivor
out, when exactly one survivor exists, or when a >=50-citation leader is
unique. Returns (paper, artifact_token)."""
years, cues = _query_cues(query)
for token in _mention_tokens(query)[:2]:
if len(token) < 3:
continue
hits = [h for h in _paper_search(client, token, _PROBE_LIMIT, inserted_before)
if not _violates_cutoff(h, inserted_before)]
if not hits:
continue
hits = [h for h in hits
if not _is_version_successor(getattr(h, "title", "") or "", token)]
tiers = [
[h for h in hits
if _leads_with_artifact(getattr(h, "title", "") or "", token)],
[h for h in hits
if _title_links_mention(getattr(h, "title", "") or "", [token])],
[h for h in hits
if token in _norm(getattr(h, "abstract", "") or "").replace(" ", "")],
]
for tier in tiers:
if not tier:
continue
if years or cues:
cued = [h for h in tier if not _fails_query_cues(query, h)]
if len(cued) == 1:
return cued[0], token
if cued:
tier = cued
# Prominence floor: a title merely BEGINNING with the artifact
# word is not evidence enough on its own (measured - an 8-citation
# "<Artifact> Applications Review" hijacked the resolution from
# the grounded reference walk). Only a well-cited, unambiguous
# leader resolves here; everything else falls through to the walk.
strong = sorted([h for h in tier if _cites(h) >= _ANCHOR_MIN_CITES],
key=_cites, reverse=True)
if len(strong) == 1:
return strong[0], token
if len(strong) > 1 and _cites(strong[0]) >= 2 * _cites(strong[1]):
return strong[0], token
return None, None
def _referenced_candidates(client: Any, cid: str) -> list:
try:
try:
refs = client.get_citations(cid, "references",
limit=_REFWALK_REF_LIMIT,
fields="corpusId,title")
except TypeError: # duck-typed client without the fields kwarg
refs = client.get_citations(cid, "references",
limit=_REFWALK_REF_LIMIT)
except Exception:
return []
return refs or []
def _reference_walk(query: str, client: Any, llm: Any,
inserted_before: str | None) -> Any | None:
"""Grounded selection over the references of the nickname's own top
hits: papers that use a system cite its canonical paper, so free recall
becomes selection over ~50 titles ranked by co-citation votes."""
seeds = []
for hit in _paper_search(client, query, _REFWALK_SEEDS, inserted_before):
cid = _cid_of(hit)
if cid and cid not in seeds:
seeds.append(cid)
if len(seeds) >= _REFWALK_SEEDS:
break
if not seeds:
return None
freq: dict[str, int] = {}
stubs: dict[str, Any] = {}
order: list[str] = []
for seed in seeds:
seen_here: set[str] = set()
for ref in _referenced_candidates(client, seed):
rcid = _cid_of(ref)
title = (getattr(ref, "title", "") or "").strip()
if not rcid or not title or rcid in seen_here:
continue
seen_here.add(rcid)
freq[rcid] = freq.get(rcid, 0) + 1
if rcid not in stubs:
stubs[rcid] = ref
order.append(rcid)
if not stubs:
return None
position = {cid: i for i, cid in enumerate(order)}
# Version-successor filter: an artifact's follow-up ('Acme2')
# is co-cited exactly where the original is, so it reaches the top of
# the vote ranking and the selector picks it. Drop successors of the
# mention's own tokens before the selector ever sees them.
tokens = [t for t in _mention_tokens(query) if len(t) >= 3]
kept = [c for c in stubs
if not any(_is_version_successor(
getattr(stubs[c], "title", "") or "", t) for t in tokens)]
pool = kept or list(stubs)
ranked = sorted(pool, key=lambda c: (-freq[c], position[c]))[:_REFWALK_TITLE_CAP]
listing = "\n".join(
f"[{i + 1}] {(getattr(stubs[c], 'title', '') or '').strip()}"
for i, c in enumerate(ranked))
user = (f"Query: {query}\n\nCandidate referenced titles ({len(ranked)}):\n{listing}"
"\n\nIf several candidates describe the same system, prefer the "
"ORIGINAL paper that introduced it over any later version, "
"extension, or follow-up.")
try:
obj = llm.json(_SELECT_SYSTEM, user, max_tokens=64) or {}
except Exception:
obj = {}
index = obj.get("index")
if isinstance(index, bool): # JSON true would coerce to candidate 1
return None
try:
index = int(index)
except (TypeError, ValueError):
return None
if not 1 <= index <= len(ranked):
return None
picked = ranked[index - 1]
paper = None
try:
paper = client.get_paper(picked)
except Exception:
paper = None
return paper if paper is not None else stubs[picked]
# --------------------------------------------------------------------------
# Contested-artifact probe (mentions that match several papers)
# --------------------------------------------------------------------------
def _leads_with_artifact(title: str, artifact_norm: str) -> bool:
"""Title's first content phrase IS the artifact name (system-paper style
'ACME: ...'; the plural form 'ACMES: ...' is allowed; leading articles
are skipped).
Version extensions ('ACME-XL: ...') are NOT the artifact."""
tn = _norm(title)
for art in _LEADING_ARTICLES:
if tn.startswith(art + " "):
tn = tn[len(art) + 1:]
if not (tn == artifact_norm or tn.startswith(artifact_norm + " ")
or tn == artifact_norm + "s" or tn.startswith(artifact_norm + "s ")):
return False
rest = tn[len(artifact_norm):].lstrip("s").strip()
if rest and rest.split()[0] in _VERSION_TOKENS:
return False
return True
def _distinct_from(paper: Any, kept: list, artifact_words: set[str]) -> bool:
"""Distinct artifact = disjoint author set AND dissimilar residual title
(shared authors or >=50% shared non-artifact content words mean the same
lineage/work: a follow-up, not a contest for the name)."""
p_authors = set(_author_tokens(paper))
p_words = _content_words(getattr(paper, "title", "") or "") - artifact_words
for other in kept:
if other is None:
continue
if p_authors and p_authors & set(_author_tokens(other)):
return False
o_words = _content_words(getattr(other, "title", "") or "") - artifact_words
if p_words and o_words:
shared = len(p_words & o_words) / min(len(p_words), len(o_words))
if shared >= _DISTINCT_TITLE_OVERLAP:
return False
return True
def _artifact_probe(query: str, artifact: str, resolved: Any | None,
extra_candidates: list, client: Any,
inserted_before: str | None) -> list:
"""Distinct corpus papers contesting the artifact name: relevance hits
(plus already-verified plausibles) whose titles LEAD with the artifact,
above a small citation floor, pairwise-distinct. The resolved paper
never appears in the returned list."""
artifact_norm = _norm(artifact)
if not artifact_norm:
return []
artifact_words = set(artifact_norm.split())
resolved_cid = _cid_of(resolved) if resolved is not None else None
pool = list(extra_candidates) + _paper_search(
client, query, _PROBE_LIMIT, inserted_before)
seen: set[str] = set()
candidates = []
for hit in pool:
cid = _cid_of(hit)
if not cid or cid in seen or cid == resolved_cid:
continue
seen.add(cid)
if _violates_cutoff(hit, inserted_before):
continue
if not _leads_with_artifact(getattr(hit, "title", "") or "", artifact_norm):
continue
if _cites(hit) < _PROBE_MIN_CITES:
continue
candidates.append(hit)
candidates.sort(key=_cites, reverse=True)
kept: list = []
for hit in candidates:
if _distinct_from(hit, kept + [resolved], artifact_words):
kept.append(hit)
if len(kept) >= _MULTI_MAX:
break
return kept
# --------------------------------------------------------------------------
# Entry point
# --------------------------------------------------------------------------
_HOMONYM_SYSTEM = """You know the scientific literature broadly. A user names an artifact with no other context, so the reference is AMBIGUOUS: several unrelated works across different fields share that name.
List the distinct, well-known papers/systems/tools/datasets that go by this exact name, ACROSS DIFFERENT RESEARCH AREAS (e.g. numerical computing, NLP, biology, neuroscience, security, systems).
Output ONLY JSON: {"works": [{"title": "<exact paper title>", "field": "<area>"}]}
Give up to 6, most notable first. Use the EXACT published title of each. If you are unsure of a title, omit that entry rather than guessing."""
def _homonym_candidates(artifact: str, llm: Any, client: Any,
inserted_before: str | None) -> list:
"""Resolve the distinct works sharing a bare artifact name.
A bare mention of a short artifact name can be genuinely ambiguous: the
right answer for such a query is the whole AMBIGUITY SET, whose members
may come from entirely unrelated research areas. Relevance search cannot
span that: it returns whatever is topically fashionable. Enumerating the
homonyms by name and verifying each against the corpus can.
Keyed on the mention text only; every candidate must resolve in the
corpus to be emitted, so the model's recall is checked, not trusted.
"""
try:
obj = llm.json(_HOMONYM_SYSTEM, f"Name: {artifact}", max_tokens=500) or {}
except Exception:
return []
out: list = []
seen: set[str] = set()
for item in (obj.get("works") or [])[:6]:
title = _clean_title(item.get("title") if isinstance(item, dict) else item)
if not title:
continue
paper = _title_search(client, title)
if paper is None or _cid_of(paper) is None:
paper = _relevance_resolve(client, title, inserted_before)
if paper is None or _cid_of(paper) is None:
continue
if _violates_cutoff(paper, inserted_before):
continue
cid = _cid_of(paper)
if cid in seen:
continue
# the resolved title must actually relate to the requested name or
# to the title we asked for -- never accept a topical near-miss
found = getattr(paper, "title", "") or ""
if not (_title_links_mention(found, [_norm(artifact).replace(" ", "")])
or _overlap(title, found) >= _TITLE_OVERLAP):
continue
seen.add(cid)
out.append(paper)
return out
def solve_specific(query: str, client: Any, llm: Any,
inserted_before: str | None = None,
trace: dict | None = None) -> Submission:
"""Resolve one navigational query to its believed exact paper set."""
if trace is None:
trace = {}
meta = _mention_meta(query, llm)
trace["meta"] = dict(meta)
mention_tokens = _mention_tokens(query)
trace["mention_tokens"] = mention_tokens
resolved: Any | None = None
link = None
verified_extras: list = [] # other verified papers (hedge/probe pool)
def _try_titles(titles: list[str], tag: str) -> None:
nonlocal resolved, link
for searched in titles[:_MAX_TITLE_PROBES]:
paper = _title_search(client, searched)
if paper is None or _cid_of(paper) is None:
continue
if _violates_cutoff(paper, inserted_before):
continue
if not _verify_meta(paper, searched, meta, mention_tokens):
continue
if resolved is None:
resolved, link = paper, tag
elif _cid_of(paper) != _cid_of(resolved):
verified_extras.append(paper)
# (b) exact title search on canonical + plausibles, verified
_try_titles([t for t in [meta["canonical"]] + meta["plausibles"] if t], "title")
# (c) guarded relevance-resolve: canonical first, then the raw query
if resolved is None:
for searched in [t for t in (meta["canonical"], query) if t]:
paper = _relevance_resolve(client, searched, inserted_before)
if paper is None or _cid_of(paper) is None:
continue
if _violates_cutoff(paper, inserted_before):
continue
if _verify_meta(paper, searched, meta, mention_tokens):
resolved, link = paper, "relevance"
break
# (c2) second chance at the canonical title with a narrow, single-question
# prompt (the combined metadata call drifts when it must emit four fields)
if resolved is None:
focused = _focused_canonical(query, llm, meta.get("canonical"))
if focused:
trace["focused_canonical"] = focused
_try_titles([focused], "title-focused")
if resolved is None:
paper = _relevance_resolve(client, focused, inserted_before)
if paper is not None and _cid_of(paper) is not None and \
not _violates_cutoff(paper, inserted_before) and \
_verify_meta(paper, focused, meta, mention_tokens):
resolved, link = paper, "relevance-focused"
# (c2b) artifact token + author cue: the link that survives a hallucinated
# canonical title WHEN the mention itself names an author.
if resolved is None:
paper, probe = _artifact_author_resolve(query, meta, client, inserted_before)
if paper is not None:
resolved, link = paper, "artifact-author"
trace["artifact_author_probe"] = probe
# (c2c) artifact token + SHORT topic phrase: models fabricate titles but
# know what an artifact is FOR, so ask only for the topic.
if resolved is None:
paper, probe = _artifact_topic_resolve(query, meta, client, llm,
inserted_before)
if paper is not None:
resolved, link = paper, "artifact-topic"
trace["artifact_topic_probe"] = probe
# (c3) artifact anchor: search the mention's OWN token, never a generated
# title - the link that survives a hallucinated canonical
if resolved is None:
paper, token = _artifact_anchor_resolve(query, client, inserted_before)
if paper is not None:
resolved, link = paper, "artifact"
trace["artifact_token"] = token
# (d) grounded reference walk (authoritative when free recall failed -
# accepted without meta-verification, like v2)
if resolved is None:
paper = _reference_walk(query, client, llm, inserted_before)
if paper is not None and _cid_of(paper) is not None and \
not _violates_cutoff(paper, inserted_before):
resolved, link = paper, "refwalk"
trace["link"] = link
trace["resolved"] = _cid_of(resolved) if resolved is not None else None
generic = _generic_acronym(query)
artifact = _bare_artifact(query)
emitted: list = []
# Contested named artifact -> multi-emit by citationCount.
# (The pre-registered "probe only off an artifact-bearing resolution"
# gate is NOT applied: the failure it targeted was caused by circular
# verification accepting an unrelated paper, which the mention-link
# requirement in _verify_meta now blocks at the source. Gating here too
# would suppress legitimate contests where the canonical title simply
# does not contain the system name at all.)
if artifact:
contested = _artifact_probe(query, artifact, resolved,
verified_extras, client, inserted_before)
# Homonym enumeration (listing the distinct works sharing a name,
# across fields) was tried here and REGRESSED the slice
# 0.780 -> 0.730: it cannot tell an AMBIGUOUS name from an
# unambiguous one, so it padded an already-correct single answer
# with extra candidates and halved that query's score.
# The helper is kept (_homonym_candidates) but not called; it
# would need an ambiguity test that fires only when several
# resolved works genuinely share the name.
trace["probe"] = [_cid_of(p) for p in contested]
if len(contested) >= _MULTI_MIN_DISTINCT:
pool = ([resolved] if resolved is not None else []) + contested
pool.sort(key=_cites, reverse=True)
emitted = ([resolved] if resolved is not None else []) + \
[p for p in pool if p is not resolved]
emitted = emitted[:_MULTI_MAX]
trace["cardinality"] = "multi-artifact"
if not emitted and resolved is not None:
if generic or _fails_query_cues(query, resolved):
extras = list(verified_extras)
if len(extras) < _HEDGE_TOTAL - 1:
extras += _paper_search(client, query, _RELEVANCE_LIMIT,
inserted_before)
emitted = [resolved]
for p in extras:
if len(emitted) >= _HEDGE_TOTAL:
break
if _violates_cutoff(p, inserted_before):
continue
if _cid_of(p) not in {_cid_of(e) for e in emitted}:
emitted.append(p)
trace["cardinality"] = "hedge3" if generic else "hedge3-cues"
else:
emitted = [resolved]
trace["cardinality"] = "single"
if not emitted:
# Nothing resolved anywhere: never emit empty - top relevance hits,
# 3 when the mention is generic or carries unverifiable cues, else 1.
top_k = _HEDGE_TOTAL if (generic or _fails_query_cues(query, None)) else 1
hits = [h for h in _paper_search(client, query, _RELEVANCE_LIMIT,
inserted_before)
if not _violates_cutoff(h, inserted_before)]
emitted = hits[:top_k]
trace["cardinality"] = f"unresolved-top{top_k}"
# Dedup by cid, cap, hydrate missing abstracts, format evidence.
final: list = []
seen: set[str] = set()
for p in emitted:
cid = _cid_of(p)
if cid and cid not in seen:
seen.add(cid)
final.append((cid, p))
final = final[:_MULTI_MAX]
hydrated = 0
out: Submission = []
for cid, p in final:
if not (getattr(p, "abstract", "") or "") and hydrated < _HYDRATE_CAP:
hydrated += 1
try:
full = client.get_paper(cid)
except Exception:
full = None
if full is not None:
if not (getattr(full, "title", "") or ""):
try:
full.title = getattr(p, "title", "") or ""
except Exception:
pass
p = full
out.append((cid, _paper_evidence(p)))
trace["emitted"] = [cid for cid, _ in out]
return out
|