File size: 11,611 Bytes
2e818da
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import pytest

from app.services.citation_graph import CitationGraph
from app.services.citation_topology import ResolvedSeed
from app.services.scholar_service import AuthorCandidate, AuthorResolution
from app.services.recommendation_service import RecommendationService
from app.services.verified_paper_discovery import VerifiedDiscoveryResult

QUERY = (
    "i want the paper Revisiting reinforce style optimization for learning from human "
    "feedback in llms. also who is its authors?"
)


class FakeClient:
    """Stands in for CerebrasClient -> returns a bare, unopinionated plan.

    The deterministic contract (_apply_deterministic_contract) is what's under test, and it
    runs *after* this LLM-shaped plan, so a minimal stub is enough here.
    """

    def structured_complete(self, messages, output_model, **kwargs):
        return output_model()


def _empty_graph() -> CitationGraph:
    return CitationGraph(project_id="project-1")


def test_single_paper_seed_extracted_from_natural_phrasing():
    seed = RecommendationService._single_paper_seed(QUERY)
    assert seed == "Revisiting reinforce style optimization for learning from human feedback in llms"


def test_single_paper_seed_ignores_plural_paper_requests():
    assert RecommendationService._single_paper_seed("recommend me some papers about attention") is None
    assert RecommendationService._single_paper_seed("find papers on contrastive learning") is None


def test_context_paper_seed_resolves_this_paper_to_the_selection():
    selection = "Naturalreasoning: Reasoning in the wild with 2.8m challenging questions"
    assert RecommendationService._context_paper_seed("/paper need this paper", selection) == selection
    assert RecommendationService._context_paper_seed("need this paper", selection) == selection
    assert RecommendationService._context_paper_seed("i want that paper", selection) == selection


def test_context_paper_seed_requires_both_reference_and_selection():
    assert RecommendationService._context_paper_seed("need this paper", None) is None
    assert RecommendationService._context_paper_seed("recommend some papers on attention", "some selection") is None


@pytest.mark.asyncio
async def test_recommend_resolves_exactly_one_named_paper(tmp_path):
    resolved = {
        "title": "Revisiting REINFORCE-Style Optimization for Learning from Human Feedback in LLMs",
        "authors": "Jane Doe, John Smith",
        "year": 2025,
        "cited_by": 3,
        "url": "https://example.com/reinforce-revisited",
        "abstract": "We revisit REINFORCE-style optimization...",
        "doi": "",
        "arxiv_id": "2501.00000",
        "openalex_id": "W123",
        "provider": "openalex",
        "can_acquire": True,
    }

    async def fake_title_resolver(title: str):
        assert "reinforce" in title.casefold()
        return dict(resolved)

    async def fake_fetcher(*args, **kwargs):
        raise AssertionError("generic discovery search should not run for a resolved single-paper request")

    service = RecommendationService(
        fetcher=fake_fetcher,
        title_resolver=fake_title_resolver,
        client=FakeClient(),
        root=str(tmp_path),
    )

    papers, plan = await service.recommend(
        "project-1",
        QUERY,
        _empty_graph(),
        [],
        use_project_context=False,
    )

    assert plan.result_limit == 1
    assert len(papers) == 1
    assert papers[0]["title"] == resolved["title"]
    assert papers[0]["authors"] == resolved["authors"]
    assert papers[0]["why_recommended"].startswith("The exact paper you asked for")


@pytest.mark.asyncio
async def test_recommend_falls_back_to_search_when_paper_unresolved(tmp_path):
    async def fake_title_resolver(title: str):
        return None

    async def fake_fetcher(query: str, n: int = 5, year_start=None, year_end=None):
        return [{
            "title": "Some Closest Match",
            "authors": "Someone",
            "year": 2024,
            "cited_by": 1,
            "url": "https://example.com/closest",
            "abstract": "",
        }]

    service = RecommendationService(
        fetcher=fake_fetcher,
        title_resolver=fake_title_resolver,
        client=FakeClient(),
        root=str(tmp_path),
    )

    papers, plan = await service.recommend(
        "project-1",
        QUERY,
        _empty_graph(),
        [],
        use_project_context=False,
    )

    assert plan.single_paper_lookup is False
    assert any("Could not confidently match" in assumption for assumption in plan.assumptions)
    assert len(papers) >= 1


@pytest.mark.asyncio
async def test_recommend_uses_selection_context_for_this_paper_requests(tmp_path):
    """Reproduces the reported bug: highlighting a paper title then typing "need this paper"
    must resolve to that highlighted paper, not generic project-graph topics."""
    selection = "Naturalreasoning: Reasoning in the wild with 2.8m challenging questions"
    resolved = {
        "title": "NaturalReasoning: Reasoning in the Wild with 2.8 Million Challenging Questions",
        "authors": "Weizhe Yuan, Jane Doe",
        "year": 2025,
        "cited_by": 7,
        "url": "https://example.com/naturalreasoning",
        "abstract": "",
    }

    async def fake_title_resolver(title: str):
        assert title == selection
        return dict(resolved)

    async def fake_fetcher(*args, **kwargs):
        raise AssertionError("generic discovery search should not run once selection context resolves the paper")

    service = RecommendationService(
        fetcher=fake_fetcher,
        title_resolver=fake_title_resolver,
        client=FakeClient(),
        root=str(tmp_path),
    )

    papers, plan = await service.recommend(
        "project-1",
        "need this paper",
        _empty_graph(),
        ["Reinforcement Learning with Verifiable Rewards and Retentive Networks"],
        use_project_context=False,
        selection_text=selection,
    )

    assert plan.result_limit == 1
    assert len(papers) == 1
    assert papers[0]["title"] == resolved["title"]


@pytest.mark.asyncio
async def test_latest_author_query_returns_only_latest_verified_work(tmp_path):
    calls = []

    async def fake_author_resolver(name: str):
        assert name == "Jonas Gehring"
        return AuthorResolution(
            status="resolved",
            candidates=[AuthorCandidate(openalex_id="A1", name=name)],
        )

    async def fake_author_fetcher(author_id: str, **kwargs):
        calls.append((author_id, kwargs))
        return [{
            "title": "Newest verified work",
            "authors": "Jonas Gehring",
            "year": 2026,
            "cited_by": 4,
            "url": "https://example.com/latest",
            "abstract": "",
            "openalex_id": "W1",
        }]

    service = RecommendationService(
        author_resolver=fake_author_resolver,
        author_fetcher=fake_author_fetcher,
        client=FakeClient(),
        root=str(tmp_path),
    )
    outcome = await service.recommend_or_clarify(
        project_id="project-1",
        query="I need the latest paper of Jonas Gehring",
        citation_graph=_empty_graph(),
        graph_labels=["Large Language Models"],
        use_project_context=True,
    )

    assert outcome.kind == "recommendations"
    assert len(outcome.papers) == 1
    assert outcome.papers[0]["title"] == "Newest verified work"
    assert calls == [("A1", {"n": 1, "year_start": None, "year_end": None, "ranking": "publication_date"})]


@pytest.mark.asyncio
async def test_ambiguous_author_returns_provider_backed_clarification(tmp_path):
    async def fake_author_resolver(name: str):
        return AuthorResolution(
            status="ambiguous",
            candidates=[
                AuthorCandidate(openalex_id="A1", name=name, works_count=10),
                AuthorCandidate(openalex_id="A2", name=name, works_count=4),
            ],
        )

    service = RecommendationService(
        author_resolver=fake_author_resolver,
        client=FakeClient(),
        root=str(tmp_path / "recommendations"),
    )
    service.clarification_store.root = str(tmp_path / "clarifications")
    service.clarification_store.__init__(root=service.clarification_store.root)

    outcome = await service.recommend_or_clarify(
        project_id="project-1",
        query="latest paper by Alex Kim",
        citation_graph=_empty_graph(),
        graph_labels=[],
        use_project_context=False,
    )

    assert outcome.kind == "clarification_required"
    assert [choice.resolved_values["author_id"] for choice in outcome.choices] == ["A1", "A2"]


@pytest.mark.asyncio
async def test_top_papers_of_year_dispatches_provider_citation_ranking(tmp_path):
    calls = []

    async def fake_ranked_fetcher(**kwargs):
        calls.append(kwargs)
        return [{
            "title": f"Ranked paper {index}", "authors": "Author", "year": 2026,
            "cited_by": 100 - index, "url": f"https://example.com/{index}", "abstract": "",
        } for index in range(5)]

    service = RecommendationService(
        ranked_fetcher=fake_ranked_fetcher,
        client=FakeClient(),
        root=str(tmp_path),
    )
    outcome = await service.recommend_or_clarify(
        project_id="project-1",
        query="What are the top 5 papers of 2026 by citation?",
        citation_graph=_empty_graph(),
        graph_labels=[],
        use_project_context=False,
    )

    assert outcome.kind == "recommendations"
    assert len(outcome.papers) == 5
    assert calls == [{"n": 5, "year_start": 2026, "year_end": 2026, "ranking": "citation_count"}]

    second = await service.recommend_or_clarify(
        project_id="project-1",
        query="What are the top 5 papers of 2026 by citation?",
        citation_graph=_empty_graph(),
        graph_labels=[],
        use_project_context=False,
    )
    assert len(second.papers) == 5
    assert len(calls) == 1


@pytest.mark.asyncio
@pytest.mark.parametrize(
    ("query", "expected_direction"),
    [
        ("top 5 papers that cite Attention Is All You Need", "cites_seed"),
        ("top 5 papers Attention Is All You Need cites", "cited_by_seed"),
    ],
)
async def test_citation_queries_dispatch_verified_direction(tmp_path, query, expected_direction):
    class FakeVerifiedDiscovery:
        def __init__(self):
            self.calls = []

        async def discover(self, **kwargs):
            self.calls.append(kwargs)
            return VerifiedDiscoveryResult(
                candidates=[{
                    "title": "Verified related paper", "authors": "Author", "year": 2025,
                    "cited_by": 10, "url": "https://example.com/verified", "abstract": "",
                    "openalex_id": "W2", "citation_verified": True,
                }],
                seed=ResolvedSeed(openalex_id="W1", title="Attention Is All You Need"),
                exhausted=True,
                provider_requests=2,
            )

    discovery = FakeVerifiedDiscovery()
    service = RecommendationService(
        verified_discovery=discovery,
        client=FakeClient(),
        root=str(tmp_path),
    )
    outcome = await service.recommend_or_clarify(
        project_id="project-1",
        query=query,
        citation_graph=_empty_graph(),
        graph_labels=["unrelated project topic"],
        use_project_context=True,
    )

    assert outcome.kind == "recommendations"
    assert discovery.calls[0]["seed_title"] == "Attention Is All You Need"
    assert discovery.calls[0]["citation_constraint"] == expected_direction