Skip to content
Merged
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ dependencies = [
"pypdf>=3.17.0",
"lxml>=4.9.0",
"matplotlib>=3.5.0",
"citeproc-py>=0.9.0",
"citeproc-py-styles>=0.1.5",
]

[dependency-groups]
Expand Down
52 changes: 47 additions & 5 deletions src/lit_agent/identifiers/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -611,17 +611,20 @@ def render_bibliography_to_strings(
}

try:
entries = list(resolution_result.citations.values())
style_obj = CitationStylesStyle(style, validate=False, locale=locale)
entries = _prepare_citeproc_entries(
resolution_result.citations.values(),
)
style_name = _resolve_csl_style(style)
style_obj = CitationStylesStyle(style_name, validate=False, locale=locale)
source = CiteProcJSON(entries)
bibliography = CitationStylesBibliography(style_obj, source, formatter.plain)

for item in source.items:
citation = Citation([CitationItem(item.id)])
for item_id in source:
citation = Citation([CitationItem(item_id)])
bibliography.register(citation)

rendered = [str(entry) for entry in bibliography.bibliography()]
return rendered, {"renderer": "citeproc-py", "style": style, "locale": locale}
return rendered, {"renderer": "citeproc", "style": style, "locale": locale}
except Exception as exc: # pragma: no cover - defensive
return _render_compact(resolution_result), {
"renderer": "fallback",
Expand Down Expand Up @@ -678,6 +681,45 @@ def _extract_year(citation: Dict[str, Any]) -> Optional[int]:
return None


def _prepare_citeproc_entries(
citations: Iterable[Dict[str, Any]],
) -> List[Dict[str, Any]]:
"""Ensure minimal CSL fields are present for citeproc rendering."""

prepared: List[Dict[str, Any]] = []
for index, citation in enumerate(citations, start=1):
normalized = dict(citation)

citation_id = normalized.get("id")
normalized["id"] = (
str(citation_id) if citation_id not in [None, ""] else str(index)
)

if not normalized.get("type"):
normalized["type"] = "article-journal"

# Remove internal metadata that citeproc does not understand
normalized.pop("resolution", None)

# citeproc expects an iterable for author names
if normalized.get("author") is None:
normalized["author"] = []

prepared.append(normalized)

return prepared


def _resolve_csl_style(style: str) -> str:
"""Map common aliases to valid CSL style identifiers."""

aliases = {
"chicago": "chicago-author-date",
}
normalized = style.lower()
return aliases.get(normalized, normalized)


def _import_citeproc():
"""Import citeproc modules, isolated for easier testing."""

Expand Down
186 changes: 186 additions & 0 deletions tests/unit/test_rendering.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,189 @@ def raise_import_error():
assert rendered[0].startswith("[1] Doe Example Paper 2024 10.1234/example")
assert meta["renderer"] == "fallback"
assert meta["style"] == "vancouver"


@pytest.mark.unit
def test_render_bibliography_vancouver_style():
"""Test proper bibliography rendering with vancouver style."""
result = CitationResolutionResult(
citations={
"1": {
"id": "1",
"title": "Example Paper on Glioblastoma",
"author": [{"family": "Doe", "given": "John"}],
"issued": {"date-parts": [[2024]]},
"container-title": "Nature",
"DOI": "10.1234/example",
"resolution": {"method": "doi"},
"URL": "https://example.com",
},
},
stats={"total": 1, "resolved": 1},
failures=[],
)

rendered, meta = render_bibliography_to_strings(result, style="vancouver")

# Should use citeproc renderer with citeproc-py-styles installed
assert meta["renderer"] == "citeproc-py", (
"Expected citeproc-py renderer but got fallback. "
"Ensure citeproc-py-styles is installed: pip install citeproc-py-styles"
)
assert meta["style"] == "vancouver"
assert meta["locale"] == "en-US"

# Bibliography should be properly formatted and non-empty
assert len(rendered) == 1, "Should have one bibliography entry"
assert rendered[0], "Bibliography entry should not be empty"
assert "Doe" in rendered[0], "Author name should appear in citation"
assert "2024" in rendered[0], "Publication year should appear"


@pytest.mark.unit
def test_render_bibliography_apa_style():
"""Test bibliography rendering with APA style."""
result = CitationResolutionResult(
citations={
"1": {
"id": "1",
"title": "Machine Learning in Cancer Research",
"author": [
{"family": "Smith", "given": "Jane"},
{"family": "Johnson", "given": "Bob"},
],
"issued": {"date-parts": [[2023]]},
"container-title": "Cell",
"DOI": "10.5678/test",
"resolution": {"method": "doi"},
},
},
stats={"total": 1, "resolved": 1},
failures=[],
)

rendered, meta = render_bibliography_to_strings(result, style="apa")

assert meta["renderer"] == "citeproc", (
Copy link

Copilot AI Dec 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The assertion checks for meta["renderer"] == "citeproc", but the actual value returned by render_bibliography_to_strings is "citeproc-py". This test will fail. The assertion should be changed to check for "citeproc-py" instead of "citeproc".

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot open a new pull request to apply changes based on this feedback

"Expected citeproc renderer. Ensure citeproc-py-styles is installed."
)
assert meta["style"] == "apa"
assert len(rendered) == 1
assert "Smith" in rendered[0]
assert "2023" in rendered[0]


@pytest.mark.unit
def test_render_bibliography_chicago_style():
"""Test bibliography rendering with Chicago style."""
result = CitationResolutionResult(
citations={
"1": {
"id": "1",
"title": "The Role of Inflammation in Disease",
"author": [{"family": "Brown", "given": "Alice"}],
"issued": {"date-parts": [[2022]]},
"container-title": "Science",
"DOI": "10.9999/chicago-test",
"resolution": {"method": "doi"},
},
},
stats={"total": 1, "resolved": 1},
failures=[],
)

rendered, meta = render_bibliography_to_strings(result, style="chicago")

assert meta["renderer"] == "citeproc-py"
assert meta["style"] == "chicago"
assert len(rendered) == 1
assert rendered[0] # Non-empty


@pytest.mark.unit
def test_render_bibliography_multiple_citations():
"""Test rendering multiple citations in correct order."""
result = CitationResolutionResult(
citations={
"1": {
"id": "1",
"title": "First Paper",
"author": [{"family": "Alpha", "given": "A"}],
"issued": {"date-parts": [[2021]]},
"DOI": "10.1111/first",
},
"2": {
"id": "2",
"title": "Second Paper",
"author": [{"family": "Beta", "given": "B"}],
"issued": {"date-parts": [[2022]]},
"DOI": "10.2222/second",
},
"3": {
"id": "3",
"title": "Third Paper",
"author": [{"family": "Gamma", "given": "C"}],
"issued": {"date-parts": [[2023]]},
"DOI": "10.3333/third",
},
},
stats={"total": 3, "resolved": 3},
failures=[],
)

rendered, meta = render_bibliography_to_strings(result, style="vancouver")

assert meta["renderer"] == "citeproc"
Copy link

Copilot AI Dec 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The assertion checks for meta["renderer"] == "citeproc", but the actual value returned by render_bibliography_to_strings is "citeproc-py". This test will fail. The assertion should be changed to check for "citeproc-py" instead of "citeproc".

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot open a new pull request to apply changes based on this feedback

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot open a new pull request to apply changes based on this feedback

assert len(rendered) == 3, "Should have three bibliography entries"

# Check all entries are non-empty
for entry in rendered:
assert entry, "Each bibliography entry should be non-empty"

# Check authors appear in their respective entries
assert "Alpha" in rendered[0]
assert "Beta" in rendered[1]
assert "Gamma" in rendered[2]


@pytest.mark.unit
def test_render_bibliography_empty_citations():
"""Test rendering with no citations."""
result = CitationResolutionResult(
citations={},
stats={"total": 0, "resolved": 0},
failures=[],
)

rendered, meta = render_bibliography_to_strings(result, style="vancouver")

# Even with no citations, should use citeproc if available
assert meta["renderer"] in ["citeproc-py", "fallback"]
assert meta["style"] == "vancouver"
assert len(rendered) == 0, "Should have no bibliography entries"


@pytest.mark.unit
def test_render_bibliography_with_locale():
"""Test bibliography rendering with different locale."""
result = CitationResolutionResult(
citations={
"1": {
"id": "1",
"title": "Example Paper",
"author": [{"family": "Doe", "given": "John"}],
"issued": {"date-parts": [[2024]]},
"DOI": "10.1234/example",
},
},
stats={"total": 1, "resolved": 1},
failures=[],
)

rendered, meta = render_bibliography_to_strings(
result, style="vancouver", locale="en-GB"
)

assert meta["renderer"] == "citeproc-py"
assert meta["locale"] == "en-GB"
assert len(rendered) == 1
17 changes: 17 additions & 0 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading