A model that reviews a contract answers in a chat window. The colleague who opens the contract next
week sees none of it. This page is about the other half: taking what the model found and writing it
into the file, as a highlight on the sentence it quoted, with its remark as the comment and a
reply thread a person can answer.
GroupDocs.Annotation for .NET does that
server-side, with no Word add-in and no browser extension, and the same code path covers PDF,
DOCX, XLSX and PPTX.
Note
A runnable implementation of this page is on GitHub:
ai-document-review-annotations-net.
It reads findings from a JSON file, or asks a model for them, and writes them into the document.
The contract between the model and the document
Keep it small. Everything this design needs from a model is a list of objects with three fields:
[{"quote":"pay all invoices within ninety days","comment":"Ninety-day payment terms are three times our standard. Ask for thirty days.","severity":"high"}]
quote is the anchor: it has to be copied verbatim from the document, because the next step
searches for it on the page. Ask for 4 to 15 words — long enough to be unique, short enough that it
does not wrap across half a page. comment becomes the annotation’s message, severity picks the
colour.
The model never sees the file format and never produces coordinates. That is what keeps the same
prompt working for a PDF, a Word file and a spreadsheet.
Step 1: read the page text with its geometry
GetDocumentInfo() returns the pages, their size, and the text lines on each with a box per line:
The same call also produces the plain text to send to the model — page by page, so the answer can
be checked against the page it came from.
Step 2: match the quote to lines
A model quotes the sentence it read. The page shows that sentence broken across two or three lines.
Matching has to allow for that:
look for a single line whose text contains the quote;
otherwise join consecutive lines and look for the quote in that window — then mark each line the
quote covers;
otherwise report the finding as unanchored. Never place it at a guess.
Normalise before comparing: collapse runs of whitespace, and fold curly quotes, en dashes and
non-breaking spaces to their plain equivalents. Models reproduce text with a tidier typography than
the document has.
Within a line, the horizontal offset can be estimated from the character position:
That assumes an even character width. It is approximate, and it never moves the highlight off the
line — which is the only property that matters here.
Step 3: convert the coordinates
Three conventions meet in this step:
Member
Origin
Page numbering
PageInfo.PageNumber, TextLineInfo
top-left
1-based
AnnotationBase.PageNumber
—
0-based
HighlightAnnotation.Points and other text markup
bottom-left
—
AreaAnnotation.Box
top-left
—
So a line at TopIndent = t with height h on a page of height H spans H - t - h … H - t,
and the page index is PageInfo.PageNumber - 1.
Warning
Passing PageInfo.PageNumber straight into AnnotationBase.PageNumber puts every annotation one
page too far — and the ones meant for the last page vanish, because an annotation addressed to a
page that does not exist is dropped when the document is saved, with no exception.
Step 4: write the annotation
usingSystem;usingSystem.Collections.Generic;usingGroupDocs.Annotation.Models;usingGroupDocs.Annotation.Models.AnnotationModels;Userreviewer=newUser{Id=1,Name="AI reviewer",Role=Role.Editor};floatleft=(float)box.X;floatright=(float)(box.X+box.Width);floattop=(float)(page.Height-box.Y);floatbottom=(float)(page.Height-box.Y-box.Height);annotator.Add(newHighlightAnnotation{PageNumber=page.PageNumber-1,BackgroundColor=0xFFE699,// amber for "medium"Opacity=0.5,Message=finding.Comment,CreatedOn=DateTime.Now,User=reviewer,Points=newList<Point>{newPoint(left,top),newPoint(right,top),newPoint(left,bottom),newPoint(right,bottom)},Replies=newList<Reply>{newReply{Comment=$"Severity: {finding.Severity}. Reply to accept or reject this remark.",RepliedOn=DateTime.Now,User=reviewer}}});annotator.Save("service-agreement.reviewed.docx");
Two details worth copying:
Do not repeat the message in the reply. Word renders both, so the same sentence would appear
twice in the margin. Use the reply to open the thread — the severity, what to do with the remark —
and let the message carry the finding.
A quote that wraps needs several rectangles in one annotation.Points is a flat list: four
points per rectangle, in the order top-left, top-right, bottom-left, bottom-right. One
HighlightAnnotation can therefore cover three lines and still be one comment.
Colours
BackgroundColor is 0xRRGGBB. A PDF keeps the exact value. Word has a fixed palette of highlight
colours and snaps to the nearest one, so a pale amber comes out as yellow there. Pick severities
that stay distinguishable after snapping — for example amber, red and light blue.
Step 5: let a human answer, and let the review be withdrawn
Because every annotation carries User.Name, the machine-written pass can be removed later without
touching anything a person added:
That is what makes the whole approach safe to try on real documents: the review is a layer, not an
edit. Replies people add live in the same thread and are read back with
Get, so a
second pass can see what was accepted.
Checking the result
Render the annotated pages and look at them — the cheapest possible test, and the one that catches a
coordinate mistake immediately:
In a test suite, assert on three things: the number of annotations written, the number of findings
that could not be anchored, and that every PageNumber is within the page count.