Construction · AI
Prokta
Tender documents read for a general contractor’s commercial department: an archive of files goes in, a report of terms comes out, every line of it with a page reference.
- Role
- Backend, document pipeline, AI assistant
- Timeline
- 9 weeks
- Stack
- Python · Qdrant · OpenAI API
What changed
- 2.3 hours → 14 minutes
- on average for the first pass over a tender: archive in, report of terms out. It used to run from an hour and a half to four.
- +37%
- to how many tenders the same commercial department gets through. Nobody was added to it.
- 22% → 8%
- of tenders were turned down only after a detailed read — that is, after the hours had already gone into them.
- 91%
- of the critical terms in the control sample the system found on its own. The remaining 9% are a person’s work.
What did not change: the document management system and the CRM, both left exactly where they were. Nor does the system decide anything — it gathers the terms with their sources, and whether to bid is the commercial director’s word.
Before
Between 180 and 250 tenders reached the commercial department every month, and a single tender ran to anywhere between forty and six hundred pages. The first pass over one took an hour and a half at best and four hours at worst: a specialist opened the files one after another, looking for the terms that make bidding a bad idea. More tenders arrived while that went on.
- Between 180 and 250 tenders a month came through the department, and every one of them had to be opened at least once: until a tender has been read, there is no telling whether it is worth bidding for.
- Forty pages in one tender, six hundred in another. Which of the two has landed is visible only from inside the documents, so a day could not be planned by counting tenders.
- The first pass took between an hour and a half and four hours — and that is only to establish what the tender is, before any of the work of bidding.
- Around 22% of tenders were dropped after that detailed read, over terms that could have been spotted at the start. Those are hours spent on tenders the company never bid for.
What we built
One way in for five formats: PDFs, DOCX, XLSX, scans and ZIP archives are unpacked, converted and recognised into pages of a single shape, each one carrying an address.
A module per section rather than a question box: requirements, deadlines, penalties, payment, guarantees, licences and volumes are each pulled out by something that looks only for them.
References are mandatory: behind every statement sits a document and a page number, and where two files disagree both places are shown.
Outcome
A tender that used to take between an hour and a half and four hours is now read in fourteen minutes: the system unpacks the archive, brings every file into one shape and assembles a report by section — bidder requirements, deadlines, penalties, payment, guarantees, licences, technical volumes, unusual contract clauses and the places where documents contradict one another. Every statement is stamped with a document and a page, so checking one means opening that page rather than rereading the tender. The share of tenders dropped only after a detailed read has gone from 22% to 8%, and the decision to bid still belongs to the commercial director.
The client
A general contractor. The system was built for its commercial department — the people who decide which tenders the company goes after. Between 180 and 250 tenders pass through them in a month, and each one needs a yes or a no.
Tenders arrive at wildly different sizes: forty pages in one, six hundred in the next, and there is no telling in advance which of them holds the clause that rules bidding out. Roughly one tender in five was dropped only after a detailed read — that is, once the hours had already gone into it.
The brief
- What the client asked for
- “A neural network that searches PDFs”: upload the documents, ask them questions in plain words, and stop paging through file after file.
- What the brief turned into
- Search answers a question, and the commercial director has none: what is buried in six hundred pages is precisely what nobody knows in advance. What was wanted was a reading — bidder requirements, deadlines, penalties, payment, guarantees, licences, unusual contract clauses, volumes, contradictions between documents and a view on the tender, all of it minutes after the archive goes in, and every statement tied to a document and a page.
Why the obvious route was closed
The volume was never the hard part. What these documents turned out to be was: five formats at the door, one condition spread across several files, and the questions nobody knows to ask out loud.
- Five formats at the door
- PDFs, DOCX, XLSX, scans and ZIP archives, and an archive could hold any of the rest. Before anything at all can be done with a document it has to be unpacked, converted and, where it is a scan, recognised — a pipeline in its own right, not a preparation step.
- Forty pages or six hundred
- Forty pages can still be handed to a model whole; six hundred cannot. The same route has to work for both, which puts retrieval over fragments at the base of it rather than one read of the whole document.
- One condition, several files
- A deadline, a penalty or a bidder requirement turned up in more than one document, worded differently each time. So no single file can be named the source of truth, and a hit in one place is not an answer until it has been checked against the rest.
- “Find everything important” is not a query
- You can ask what the warranty period in a tender is. You cannot ask what is dangerous here: that question has no shape to search on. So the list of what had to come out was fixed in advance and extracted item by item, rather than asked for.
- The page number has to survive the pipeline
- A reference has to outlive unpacking, conversion and recognition. So the address — file and page — is assigned on the way in and travels with every fragment as far as the report: assign it later and there is nothing left to assign it from.
- The tenth item is a judgement
- Nine items in the report can be extracted from the pages. The tenth, how attractive the tender is, cannot: it is a conclusion. That is where the system stops — it gathers the grounds, and whether to bid is decided by a person.
How the work ran
- 01
The ingestion pipeline
Weeks 1–2Unpacking archives, converting DOCX and XLSX through headless LibreOffice, OCR for the scans, originals kept in object storage. This is also where every page is given its address — file and number — and it does not change afterwards.
- 02
Structure and retrieval
Weeks 3–4Documents are broken into sections and fragments, and the fragments go into a vector index. Retrieval by meaning is here for one reason: the same condition is written two different ways in two files, and matching on words finds one wording of the two.
- 03
Extraction by section
Week 5Bidder requirements, deadlines, penalties, payment, guarantees, licences and technical volumes each got a module of their own, with its own query and its own answer shape. A module returns fields with page references, not prose.
- 04
Comparison and contradictions
Week 6What comes out of different files is brought together by meaning and compared: one deadline against another, one figure against another. The system does not settle a disagreement — it shows both places and both references.
- 05
The assistant and the report
Week 7An agent assembles the modules’ output into a single report: sections, the terms found, the contradictions and a view on the tender. Questions to the uploaded documents in plain words live here too — the thing the client asked for at the start.
- 06
Testing against the archive
Weeks 8–91,200 old tenders — the archive the department had already read by hand — went through the system. The two weeks went on checking it against a control sample: the system found 91% of the critical terms in it on its own, and the rest stayed with a person.
Technical decisions
The pipeline has three parts, and the order between them matters more than any one of them: Python runs the document pipeline, Qdrant answers for retrieval by meaning, and the models behind the OpenAI API turn what is retrieved into fields of a report.
Ingestion apart from analysis
Unpacking, conversion and recognition are a stage with an output of their own: pages of one shape, each with a source file and a number. Nothing further down the line knows whether a page came from a DOCX, a scan or a spreadsheet, and OCR failing on one file does not take the whole tender with it.
The page address is a required field
A document and a page are attached on the way in, travel with the fragment through the index and the model, and land in the report. A statement without a source does not get in: in a tender review an unverifiable line is worse than a gap, because somebody will act on it.
Retrieval by meaning, not by words
Fragments sit in Qdrant because the same condition is phrased differently in two documents and both occurrences are wanted. Retrieval returns every place a condition appears rather than the best single answer: comparing them is the next step.
A module per section instead of one query
Each section of the report has a module of its own, with its own query and its own answer shape. One query over everything produces fluent text and drops items quietly; a module that looks only for penalties either finds them or does not, and that is visible.
Contradictions are shown, not settled
Where two documents in the same tender name different deadlines, the system does not pick the correct one: it sets them side by side with a reference each. Resolving a disagreement means deciding which document to believe, and that is no longer extraction — it is part of deciding whether to bid.
The agent sits on top of the modules
The final report is put together by an agent that does not read the tender again: it works from what the modules returned, so the view on a tender rests on the terms found rather than on an impression of the text. Model calls go through one layer — there are two providers in the stack, and a module has no business knowing which of them it is talking to.
What stayed outside the scope
Part of the work was never taken on, and the line was drawn while the brief was being pulled apart rather than after launch.
Document management. Files arrive where they always arrived: the system takes an archive in and hands a report back, while storage and routing stayed where they were.
The CRM. Untouched and not replaced — what happens to a tender after the decision to bid lives where it always lived.
The decision to bid. The system goes as far as a view on the tender with its grounds attached, and stops there. There is no bid button in it.
The remaining 9% of critical terms. The control sample put the system at 91%, which is to say the report does not stand in for reading the contract — it says which pages to start on.
The system sits on the live flow: a tender goes through it before anyone opens it, and by the time someone does, the report by section, references and all, is already there.
More projects
Restaurants
Ostera
A chain of 64 restaurants: figures from five systems in one layer, where a question about yesterday’s profit is asked in plain words and answered with the data behind it.
64 restaurants
counting on one set of definitions
NestJS · ClickHouse · OpenAI API
Insurance
Klarim
Reads the scanned pack behind an insurance claim: finds the fields in certificates, photos and statements and moves them into the claims system.
40 → 6 minutes
to process a single claim
Python · FastAPI · Anthropic API
Contact
Send a description of the task
A reply with the scope, the timeline and a budget estimate comes within 24 hours.
The first call is 30 minutes, with no commitment on your side.