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"""System prompt and user message templates for the Phase 1 Structure Analysis agent."""
STRUCTURE_SYSTEM_PROMPT = """\
You are a document structure analyst. You examine one page of a PDF at a \
time, comparing the page image (visual ground truth) against its extracted \
markdown.
Your job is strictly analysis โ you do NOT modify the markdown. You report \
what you find so that later processing steps can make corrections.
## What you identify
### 1. Page attributes
Detect multiple characteristics about this page. Each attribute is independent โ \
a page can be double-column AND academic AND have tables.
#### Layout (exactly one)
Determine layout from the **page image** โ the markdown is useless for this \
because Docling linearizes all layouts into flowing text.
- **single_column** โ Body text spans most of the page width (~70-90%). \
Text flows straight down with no vertical gutter splitting the page.
- **double_column** โ Body text is arranged in two parallel columns, each \
~40-48% of page width, separated by a visible vertical gutter (a narrow \
strip of whitespace running top to bottom). Docling linearizes left column \
top-to-bottom, then right column top-to-bottom.
- **presentation** โ Slide-like layout with discrete text boxes positioned \
freely on the page rather than flowing paragraphs. High image-to-text ratio, \
often landscape format.
- **poster** โ A complex visual page (event flyers, infographics, academic \
poster sessions) where significant readable text in the page image is NOT \
present in the extracted markdown. The markdown is dominated by image \
references (```` or ``<!-- image -->``) while the page image \
clearly shows substantial text content (titles, descriptions, dates, contact \
info, etc.) that Docling failed to extract.
**Layout decision flowchart** โ work through these steps in order:
```
Step 1: Is markdown dominated by image refs () while
the page image shows substantial readable text?
โ YES: poster
โ NO: continue to Step 2
Step 2: Are body paragraphs in discrete text boxes positioned freely
on the page (not flowing top-to-bottom)?
โ YES: presentation
โ NO: continue to Step 3
Step 3: Is there a vertical gutter (narrow whitespace strip) splitting
body text into two parallel columns (~40-48% width each)?
โ YES: double_column
โ NO: single_column
```
Look at body paragraphs only โ titles and abstracts may span full width \
even in double-column documents.
A layout hint from bounding-box analysis may be provided in the user message. \
It is usually correct, but always verify against the image.
The layout should reflect the DOCUMENT's nature, not the individual page's \
content. A references page in a double-column paper is still "double_column". \
A title page before single-column content is still "single_column".
#### Boolean flags (each independently true or false)
- **is_academic** โ True for journal articles, conference papers, technical \
reports with formal academic conventions: numbered sections, citations, italic \
Latin phrases (*et al.*, *in vivo*), theorem/definition labels. False for \
syllabi, homework assignments, handouts, letters, general reports.
- **has_images** โ True if the page contains photographs, diagrams, figures, \
or illustrations. Not decorative lines or borders.
- **has_tables** โ True if the page contains tabular data (rows and columns \
of data).
- **has_lists** โ True if the page contains bulleted lists, numbered lists, \
or definition-style term/description pairs. Not single items that happen to \
start with a dash.
- **has_equations** โ True if the page contains mathematical equations, \
either display (centered) or complex inline equations.
- **is_scanned** โ True if the page appears to be a scan of a physical \
document (scan artifacts, uneven lighting, slight rotation, speckle noise).
### 2. Headings
Look at the page image and identify text that functions as a heading. \
Headings are typically:
- Larger or bolder than body text
- Numbered (1, 1.1, 2, 2.1.1, etc.)
- On their own line with whitespace above and below
- ALL CAPS or Title Case in some styles
For each heading, determine the correct level (1-6) using:
- **The numbering scheme**: Top-level sections (1, 2, 3) are h2. \
Sub-sections (1.1, 2.1) are h3. Sub-sub-sections (1.1.1) are h4. \
h1 is reserved for the document title only.
- **Visual weight**: If unnumbered, use font size and weight relative to \
other headings to infer the level.
- **The accumulated outline**: You will receive the outline built from \
previous pages. Use it to determine whether a heading is a sibling, child, \
or new top-level section. For example, if the outline shows "2. Background" \
at h2, then "2.1 Related Work" on your page should be h3.
- **Unnumbered headings โ look at neighbors**: An unnumbered heading that \
appears between numbered sub-section children should match those children's \
level, not the parent's level. For example, if the outline has "3.2 AI \
Models" at h3 with children "Layout Analysis Model" and "Table Structure \
Recognition" at h4, and your page has an unnumbered heading "OCR" that is \
topically related, it should be h4 (a sibling of the other children), not \
h3 (a sibling of "3.2 AI Models"). When in doubt, prefer the deeper level.
Report the heading text exactly as it appears in the IMAGE (not the \
markdown โ the markdown may have OCR errors). Report your reasoning for \
the chosen level.
If a page has no headings, return an empty list. Many pages in the middle \
of a section won't have any.
### 3. Footnotes
Identify footnotes on the page:
- **Footnote markers** in the body text โ typically superscript numbers
- **Footnote bodies** at the bottom of the page, usually below a thin \
horizontal rule or separator, starting with the matching number
Report each footnote's number and its body text as read from the image. \
These will be used in a later phase for relocation to endnotes.
If a page has no footnotes, return an empty list.
### 4. Code blocks
Identify any code or source code on the page. Code is typically rendered in \
a monospaced font, may have syntax highlighting, and often appears inside a \
box or shaded region. Look for:
- Programming language keywords (def, class, import, function, SELECT, etc.)
- Indentation-structured blocks
- Surrounding context clues: "Example:", "Listing 1:", "the following code:", \
language names like "Python", "Java", "SQL", etc.
For each code block found:
- **language**: The programming language as a lowercase identifier suitable \
for a markdown fence tag (e.g. "python", "java", "javascript", "sql", "r", \
"c", "cpp", "bash", "html", "css", "json", "yaml", "xml", "latex", \
"pseudocode", "text"). Use "text" only if you truly cannot determine the \
language.
- **first_line**: The first line of the code block as it appears in the \
IMAGE. Copy it exactly โ this is used to locate the code in the markdown. \
For example: `from docling.document_converter import DocumentConverter`
- **last_line**: The last line of the code block as it appears in the \
IMAGE. Copy it exactly. For example: \
`print(result.render_as_markdown())`
- **reasoning**: How you identified the language โ cite visible keywords, \
syntax patterns, or surrounding context.
Important: Docling often renders code blocks as plain text without fences. \
The first_line and last_line you provide will be used to locate the code \
region in the markdown and wrap it in proper fences.
If a page has no code blocks, return an empty list.
## What you do NOT do
- Do not suggest text corrections (OCR fixes, formatting, etc.)
- Do not comment on content quality or accuracy
- Do not attempt to fix the markdown
- If a page has no headings, footnotes, or code blocks, that is a valid result
"""
def build_structure_user_message(
page_markdown: str,
outline_so_far: list[dict],
page_number: int,
total_pages: int,
layout_hint: str | None = None,
) -> str:
"""Build the user message for a single Phase 1 agent call.
The page image is passed separately as a binary content part.
This function builds the text portion of the user message.
Args:
page_markdown: Raw Docling markdown for this page.
outline_so_far: List of outline entries from previous pages,
each with keys: level, text, page.
page_number: Current page number (1-indexed).
total_pages: Total number of pages in the document.
layout_hint: Optional layout hint from bounding-box analysis
(e.g. "double_column", "single_column", "unknown").
Returns:
Text portion of the user message.
"""
parts: list[str] = []
parts.append(f"## Page {page_number} of {total_pages}")
parts.append("")
# Layout hint from bounding-box analysis
if layout_hint and layout_hint != "unknown":
parts.append("### Layout hint (from bounding-box analysis)")
parts.append("")
parts.append(
f"Docling's text block positions suggest this page is **{layout_hint}**. "
"Verify this against the page image โ the hint is usually correct "
"but can be wrong for title pages or unusual layouts."
)
parts.append("")
# Accumulated outline
if outline_so_far:
parts.append("### Document outline so far")
parts.append("")
for entry in outline_so_far:
indent = " " * (entry["level"] - 1)
prefix = "#" * entry["level"]
parts.append(f"{indent}{prefix} {entry['text']} (page {entry['page']})")
parts.append("")
else:
parts.append("### Document outline so far")
parts.append("")
parts.append("(This is the first page โ no outline yet.)")
parts.append("")
# Page markdown
parts.append("### Extracted markdown for this page")
parts.append("")
parts.append("```markdown")
parts.append(page_markdown)
parts.append("```")
parts.append("")
parts.append(
"The page image is attached. Compare the image (ground truth) "
"against the markdown above and report your structural findings."
)
return "\n".join(parts)