Skip to content

Commit 49f14ca

Browse files
Merge pull request #38 from codefrydev/RichPdf
Rich pdf
2 parents 89c6169 + 134c001 commit 49f14ca

52 files changed

Lines changed: 4315 additions & 2119 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/MCP.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ Export tools write artifact files with a 24-hour TTL; in-app chat renders downlo
214214

215215
### Export and deliverables
216216

217-
`export_audit_report`, `export_compare_csv`, `export_list_as_csv`, `export_sitemap_xml`, `validate_rich_results`, `compose_custom_report`, `export_custom_report`, `list_export_formats`
217+
`export_audit_report`, `export_compare_csv`, `export_list_as_csv`, `export_sitemap_xml`, `validate_rich_results`, `list_export_formats`
218218

219219
Full audit exports use the same generators as the Export view. PDF export requires `reportlab`.
220220

src/website_profiling/llm/agent.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,6 @@ def _max_tool_rounds(cfg: dict[str, str]) -> int:
7979
- Full audit PDF/HTML/CSV/JSON: export_audit_report with format pdf|html|csv|json
8080
- Compare issue diff CSV: export_compare_csv with baseline_report_id
8181
- Export a list as CSV: export_list_as_csv with tool_name and tool_args (e.g. list_broken_links)
82-
- Custom client report: compose_custom_report with title and sections (executive_summary, category_scores, tool, notes), then export_custom_report format=pdf or html
8382
- After export tools succeed, tell the user their download is ready; the UI renders file buttons automatically
8483
8584
Visualization playbook (chat UI renders charts and tables from tool JSON automatically):
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
"""PDF document model and export pipeline."""
2+
from __future__ import annotations
3+
4+
from .builder import build_pdf_document
5+
from .document import PdfDocument
6+
from .options import PdfBuildOptions, PdfLimits
7+
from .render import render_pdf_document
8+
9+
__all__ = [
10+
"build_pdf_document",
11+
"render_pdf_document",
12+
"PdfDocument",
13+
"PdfBuildOptions",
14+
"PdfLimits",
15+
]
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"""Section adapter registry.
2+
3+
Each adapter maps a section key to a function that accepts the raw payload
4+
dict + PdfBuildOptions and returns a list of PdfSection objects. Adapters
5+
that find no relevant data return an empty list.
6+
"""
7+
from __future__ import annotations
8+
9+
from typing import Any, Callable
10+
11+
from ..document import PdfSection
12+
from ..options import PdfBuildOptions
13+
14+
SectionAdapterFn = Callable[[dict[str, Any], PdfBuildOptions], list[PdfSection]]
15+
16+
# Populated by each sub-module at import time
17+
SECTION_ADAPTERS: dict[str, SectionAdapterFn] = {}
18+
19+
20+
def register(key: str) -> Callable[[SectionAdapterFn], SectionAdapterFn]:
21+
"""Decorator: @register("lighthouse") marks a function as a section adapter."""
22+
def _wrap(fn: SectionAdapterFn) -> SectionAdapterFn:
23+
SECTION_ADAPTERS[key] = fn
24+
return fn
25+
return _wrap
26+
27+
28+
# Import adapters so they self-register
29+
from . import core, findings, appendix # noqa: E402, F401
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
"""Appendix adapter — crawled URL sample and data-source glossary."""
2+
from __future__ import annotations
3+
4+
from typing import Any
5+
6+
from ....tools.export_audit_data import _GLOSSARY_ROWS
7+
from ..document import (
8+
KeyValueBlock,
9+
PdfSection,
10+
PdfTruncation,
11+
SpacerBlock,
12+
UrlListBlock,
13+
)
14+
from ..options import PdfBuildOptions
15+
from . import register
16+
17+
18+
@register("appendix")
19+
def adapt_appendix(payload: dict[str, Any], opts: PdfBuildOptions) -> list[PdfSection]:
20+
if not opts.include_appendix:
21+
return []
22+
23+
sections: list[PdfSection] = []
24+
25+
# --- Crawled URLs sample ---
26+
links = [l for l in (payload.get("links") or []) if isinstance(l, dict)]
27+
if links:
28+
limit = opts.limits.urls_sample
29+
sample = links[:limit]
30+
rows = [
31+
{
32+
"url": str(lnk.get("url") or ""),
33+
"status": str(lnk.get("status") or ""),
34+
"title": str(lnk.get("title") or "").strip(),
35+
}
36+
for lnk in sample
37+
]
38+
has_titles = any(r["title"] for r in rows)
39+
trunc = PdfTruncation(shown=len(rows), total=len(links)) if len(links) > limit else None
40+
sections.append(PdfSection(
41+
id="appendix.urls",
42+
section_key="links",
43+
title="Crawled URLs (sample)",
44+
priority=80,
45+
page_break_before=False,
46+
blocks=[
47+
UrlListBlock(
48+
id="appendix.url_list",
49+
rows=rows,
50+
show_title=has_titles,
51+
truncation=trunc,
52+
),
53+
SpacerBlock(id="appendix.url_spacer", height_pt=6),
54+
],
55+
))
56+
57+
# --- Glossary ---
58+
if opts.include_glossary:
59+
gloss_rows = [(term, desc) for term, desc in _GLOSSARY_ROWS]
60+
sections.append(PdfSection(
61+
id="appendix.glossary",
62+
section_key="core",
63+
title="Data source glossary",
64+
priority=90,
65+
blocks=[KeyValueBlock(id="appendix.glossary_kv", rows=gloss_rows, layout="glossary")],
66+
))
67+
68+
return sections
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
"""Core adapter — audit-details section (category scores live on cover)."""
2+
from __future__ import annotations
3+
4+
from typing import Any
5+
6+
from ....tools.export_audit_data import _format_report_date, _summary_lines
7+
from ..document import KeyValueBlock, PdfSection, SpacerBlock
8+
from ..options import PdfBuildOptions
9+
from . import register
10+
11+
12+
@register("core")
13+
def adapt_core(payload: dict[str, Any], opts: PdfBuildOptions) -> list[PdfSection]:
14+
sections: list[PdfSection] = []
15+
16+
# Category scores are rendered on the cover page — not duplicated here.
17+
18+
# --- Audit details section ---
19+
summary_rows = _summary_lines(payload)
20+
if summary_rows:
21+
formatted_rows: list[tuple[str, str]] = []
22+
for key, val in summary_rows:
23+
if key == "Report generated":
24+
formatted_rows.append((key, _format_report_date(val)))
25+
else:
26+
formatted_rows.append((key, val))
27+
sections.append(PdfSection(
28+
id="core.audit_details",
29+
section_key="core",
30+
title="Audit details",
31+
priority=70,
32+
blocks=[
33+
KeyValueBlock(id="core.audit_kv", rows=formatted_rows, layout="audit"),
34+
SpacerBlock(id="core.audit_spacer", height_pt=6),
35+
],
36+
))
37+
38+
return sections
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
"""Findings adapter — normalizes and groups all audit issues."""
2+
from __future__ import annotations
3+
4+
from typing import Any
5+
6+
from ....tools.export_audit_data import _issues_rows, _priority_sort_key
7+
from ..document import PdfSection, PdfTruncation
8+
from ..normalize import group_issues_for_pdf, normalize_issue_for_pdf
9+
from ..options import PdfBuildOptions
10+
from . import register
11+
12+
13+
@register("findings")
14+
def adapt_findings(payload: dict[str, Any], opts: PdfBuildOptions) -> list[PdfSection]:
15+
raw_rows = _issues_rows(payload)
16+
if not raw_rows:
17+
return []
18+
19+
raw_rows = sorted(raw_rows, key=_priority_sort_key)
20+
total = len(raw_rows)
21+
capped = raw_rows[: opts.limits.issues_total]
22+
23+
pdf_issues = [
24+
normalize_issue_for_pdf(row, include_recommendation=opts.include_recommendations)
25+
for row in capped
26+
]
27+
28+
groups = group_issues_for_pdf(
29+
pdf_issues,
30+
issues_per_group=opts.limits.issues_per_group,
31+
issues_total=opts.limits.issues_total,
32+
)
33+
34+
if not groups:
35+
return []
36+
37+
section_trunc: PdfTruncation | None = None
38+
if total > opts.limits.issues_total:
39+
section_trunc = PdfTruncation(
40+
shown=opts.limits.issues_total,
41+
total=total,
42+
reason="limit",
43+
continue_in=["CSV", "workbook"],
44+
)
45+
46+
return [PdfSection(
47+
id="findings",
48+
section_key="findings",
49+
title="Findings",
50+
priority=20,
51+
page_break_before=False,
52+
blocks=list(groups), # type: ignore[arg-type]
53+
truncation=section_trunc,
54+
)]

0 commit comments

Comments
 (0)